{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Text classification and sentiment analysis: Yelp Reviews"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once text data has been converted into numerical features using the natural language processing techniques discussed in the previous sections, text classification works just like any other classification task.\n",
    "\n",
    "In this notebook, we will apply these preprocessing technique to Yelp business reviews to classify them by review scores and sentiment polarity."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:37:13.006374Z",
     "start_time": "2018-11-26T06:37:12.515786Z"
    }
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import warnings\n",
    "from collections import Counter, OrderedDict\n",
    "from pathlib import Path\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from pandas.io.json import json_normalize\n",
    "import pyarrow as pa   \n",
    "import pyarrow.parquet as pq\n",
    "from fastparquet import ParquetFile \n",
    "from scipy import sparse\n",
    "from scipy.spatial.distance import pdist, squareform\n",
    "\n",
    "# Visualization\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.ticker import FuncFormatter, ScalarFormatter\n",
    "import seaborn as sns\n",
    "\n",
    "# spacy, textblob and nltk for language processing\n",
    "from textblob import TextBlob, Word\n",
    "\n",
    "# sklearn for feature extraction & modeling\n",
    "from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.naive_bayes import MultinomialNB\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score, roc_curve, accuracy_score, confusion_matrix\n",
    "from sklearn.externals import joblib\n",
    "\n",
    "import lightgbm as lgb\n",
    "\n",
    "import json\n",
    "from time import clock, time"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:37:13.010613Z",
     "start_time": "2018-11-26T06:37:13.007802Z"
    }
   },
   "outputs": [],
   "source": [
    "plt.style.use('fivethirtyeight')\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Yelp Challenge: business reviews dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we apply sentiment analysis to the significantly larger Yelp business review dataset with five outcome classes. \n",
    "\n",
    "The data consists of several files with information on the business, the user, the review and other aspects that Yelp provides to encourage data science innovation. \n",
    "\n",
    "We will use around six million reviews produced over the 2010-2018 period. In addition to the text features resulting from the review texts, we will also use other information submitted with the review about the user. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The [Yelp](https://www.yelp.com/dataset) dataset covers a subset of Yelp's businesses, reviews, and user data. \n",
    "\n",
    "You can download the data come in json format after accepting the license. It contains 3.6GB (compressed) and around 9GB (uncompressed) of text data. \n",
    "\n",
    "After download, extract the user.json and reviews.json files into to `data/yelp/json`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set up data directories"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We parse the json files and store the result in parquet format in our cental data directory so we can reuse the cleaned data. You can remove the large json files after parsing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_dir = Path('../data')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:37:18.194551Z",
     "start_time": "2018-11-26T06:37:18.192238Z"
    }
   },
   "outputs": [],
   "source": [
    "yelp_dir = Path('data', 'yelp')\n",
    "parquet_dir = data_dir / 'yelp'\n",
    "if not parquet_dir.exists():\n",
    "    parquet_dir.mkdir(exist_ok=True)\n",
    "text_features_dir = yelp_dir / 'text_features'\n",
    "if not text_features_dir.exists():\n",
    "    text_features_dir.mkdir(exist_ok=True)    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Parse json and store as parquet files"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:31:03.603964Z",
     "start_time": "2018-11-21T16:25:46.210795Z"
    },
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "review\n",
      "user\n"
     ]
    }
   ],
   "source": [
    "for file in ['review', 'user']:\n",
    "    print(file)\n",
    "    json_file = yelp_dir / 'json' / f'{file}.json'\n",
    "    parquet_file = parquet_dir / f'{file}.parquet'\n",
    "\n",
    "    data = json_file.read_text(encoding='utf-8')\n",
    "    json_data = '[' + ','.join([l.strip()\n",
    "                                for l in data.split('\\n') if l.strip()]) + ']\\n'\n",
    "    data = json.loads(json_data)\n",
    "    df = json_normalize(data)\n",
    "    if file == 'review':\n",
    "        df.date = pd.to_datetime(df.date)\n",
    "        latest = df.date.max()\n",
    "        df['year'] = df.date.dt.year\n",
    "        df['month'] = df.date.dt.month\n",
    "        df = df.drop(['date', 'business_id', 'review_id'], axis=1)\n",
    "    if file == 'user':\n",
    "        df.yelping_since = pd.to_datetime(df.yelping_since)\n",
    "        df = (df.assign(member_yrs=lambda x: (latest - x.yelping_since)\n",
    "                        .dt.days.div(365).astype(int))\n",
    "              .drop(['elite', 'friends', 'name', 'yelping_since'], axis=1))\n",
    "    df.dropna(how='all', axis=1).to_parquet(parquet_file)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:35:47.922755Z",
     "start_time": "2018-11-21T16:35:47.140603Z"
    },
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "RangeIndex: 1637138 entries, 0 to 1637137\n",
      "Data columns (total 19 columns):\n",
      "average_stars         1637138 non-null float64\n",
      "compliment_cool       1637138 non-null int64\n",
      "compliment_cute       1637138 non-null int64\n",
      "compliment_funny      1637138 non-null int64\n",
      "compliment_hot        1637138 non-null int64\n",
      "compliment_list       1637138 non-null int64\n",
      "compliment_more       1637138 non-null int64\n",
      "compliment_note       1637138 non-null int64\n",
      "compliment_photos     1637138 non-null int64\n",
      "compliment_plain      1637138 non-null int64\n",
      "compliment_profile    1637138 non-null int64\n",
      "compliment_writer     1637138 non-null int64\n",
      "cool                  1637138 non-null int64\n",
      "fans                  1637138 non-null int64\n",
      "funny                 1637138 non-null int64\n",
      "review_count          1637138 non-null int64\n",
      "useful                1637138 non-null int64\n",
      "user_id               1637138 non-null object\n",
      "member_yrs            1637138 non-null int64\n",
      "dtypes: float64(1), int64(17), object(1)\n",
      "memory usage: 237.3+ MB\n"
     ]
    }
   ],
   "source": [
    "user = pd.read_parquet(parquet_dir / 'user.parquet')\n",
    "user.info(null_counts=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:35:51.985181Z",
     "start_time": "2018-11-21T16:35:51.970430Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>average_stars</th>\n",
       "      <th>compliment_cool</th>\n",
       "      <th>compliment_cute</th>\n",
       "      <th>compliment_funny</th>\n",
       "      <th>compliment_hot</th>\n",
       "      <th>compliment_list</th>\n",
       "      <th>compliment_more</th>\n",
       "      <th>compliment_note</th>\n",
       "      <th>compliment_photos</th>\n",
       "      <th>compliment_plain</th>\n",
       "      <th>compliment_profile</th>\n",
       "      <th>compliment_writer</th>\n",
       "      <th>cool</th>\n",
       "      <th>fans</th>\n",
       "      <th>funny</th>\n",
       "      <th>review_count</th>\n",
       "      <th>useful</th>\n",
       "      <th>user_id</th>\n",
       "      <th>member_yrs</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>4.03</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "      <td>25</td>\n",
       "      <td>5</td>\n",
       "      <td>17</td>\n",
       "      <td>95</td>\n",
       "      <td>84</td>\n",
       "      <td>l6BmjZMeQD3rDxWUbiAiow</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>3.63</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>16</td>\n",
       "      <td>4</td>\n",
       "      <td>22</td>\n",
       "      <td>33</td>\n",
       "      <td>48</td>\n",
       "      <td>4XChL029mKr5hydo79Ljxg</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3.71</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>10</td>\n",
       "      <td>0</td>\n",
       "      <td>8</td>\n",
       "      <td>16</td>\n",
       "      <td>28</td>\n",
       "      <td>bc8C_eETBWL0olvFSJJd0w</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4.85</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>14</td>\n",
       "      <td>5</td>\n",
       "      <td>4</td>\n",
       "      <td>17</td>\n",
       "      <td>30</td>\n",
       "      <td>dD0gZpBctWGdWo9WlGuhlA</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4.08</td>\n",
       "      <td>80</td>\n",
       "      <td>0</td>\n",
       "      <td>80</td>\n",
       "      <td>28</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>16</td>\n",
       "      <td>5</td>\n",
       "      <td>57</td>\n",
       "      <td>0</td>\n",
       "      <td>25</td>\n",
       "      <td>665</td>\n",
       "      <td>39</td>\n",
       "      <td>279</td>\n",
       "      <td>361</td>\n",
       "      <td>1114</td>\n",
       "      <td>MM4RJAeH6yuaN8oZDSt0RA</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   average_stars  compliment_cool  compliment_cute  compliment_funny  \\\n",
       "0           4.03                1                0                 1   \n",
       "1           3.63                1                0                 1   \n",
       "2           3.71                0                0                 0   \n",
       "3           4.85                0                0                 0   \n",
       "4           4.08               80                0                80   \n",
       "\n",
       "   compliment_hot  compliment_list  compliment_more  compliment_note  \\\n",
       "0               2                0                0                1   \n",
       "1               1                0                0                0   \n",
       "2               0                0                0                1   \n",
       "3               1                0                0                0   \n",
       "4              28                1                1               16   \n",
       "\n",
       "   compliment_photos  compliment_plain  compliment_profile  compliment_writer  \\\n",
       "0                  0                 1                   0                  2   \n",
       "1                  0                 0                   0                  0   \n",
       "2                  0                 0                   0                  0   \n",
       "3                  0                 2                   0                  1   \n",
       "4                  5                57                   0                 25   \n",
       "\n",
       "   cool  fans  funny  review_count  useful                 user_id  member_yrs  \n",
       "0    25     5     17            95      84  l6BmjZMeQD3rDxWUbiAiow           5  \n",
       "1    16     4     22            33      48  4XChL029mKr5hydo79Ljxg           5  \n",
       "2    10     0      8            16      28  bc8C_eETBWL0olvFSJJd0w           5  \n",
       "3    14     5      4            17      30  dD0gZpBctWGdWo9WlGuhlA           4  \n",
       "4   665    39    279           361    1114  MM4RJAeH6yuaN8oZDSt0RA           5  "
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "user.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:36:18.752582Z",
     "start_time": "2018-11-21T16:35:55.329305Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "RangeIndex: 6685900 entries, 0 to 6685899\n",
      "Data columns (total 8 columns):\n",
      "cool       6685900 non-null int64\n",
      "funny      6685900 non-null int64\n",
      "stars      6685900 non-null float64\n",
      "text       6685900 non-null object\n",
      "useful     6685900 non-null int64\n",
      "user_id    6685900 non-null object\n",
      "year       6685900 non-null int64\n",
      "month      6685900 non-null int64\n",
      "dtypes: float64(1), int64(5), object(2)\n",
      "memory usage: 408.1+ MB\n"
     ]
    }
   ],
   "source": [
    "review = pd.read_parquet(parquet_dir / 'review.parquet')\n",
    "review.info(null_counts=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Merge review and user files"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:41:33.291590Z",
     "start_time": "2018-11-21T16:37:54.568008Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "Int64Index: 6685900 entries, 0 to 6685899\n",
      "Data columns (total 25 columns):\n",
      "cool                  6685900 non-null int64\n",
      "funny                 6685900 non-null int64\n",
      "stars                 6685900 non-null float64\n",
      "text                  6685900 non-null object\n",
      "useful                6685900 non-null int64\n",
      "year                  6685900 non-null int64\n",
      "month                 6685900 non-null int64\n",
      "average_stars         6685900 non-null float64\n",
      "compliment_cool       6685900 non-null int64\n",
      "compliment_cute       6685900 non-null int64\n",
      "compliment_funny      6685900 non-null int64\n",
      "compliment_hot        6685900 non-null int64\n",
      "compliment_list       6685900 non-null int64\n",
      "compliment_more       6685900 non-null int64\n",
      "compliment_note       6685900 non-null int64\n",
      "compliment_photos     6685900 non-null int64\n",
      "compliment_plain      6685900 non-null int64\n",
      "compliment_profile    6685900 non-null int64\n",
      "compliment_writer     6685900 non-null int64\n",
      "cool_user             6685900 non-null int64\n",
      "fans                  6685900 non-null int64\n",
      "funny_user            6685900 non-null int64\n",
      "review_count          6685900 non-null int64\n",
      "useful_user           6685900 non-null int64\n",
      "member_yrs            6685900 non-null int64\n",
      "dtypes: float64(2), int64(22), object(1)\n",
      "memory usage: 1.3+ GB\n"
     ]
    }
   ],
   "source": [
    "combined = (review\n",
    "            .merge(user, on='user_id', how='left', suffixes=['', '_user'])\n",
    "            .drop('user_id', axis=1))\n",
    "combined = combined[combined.stars > 0]\n",
    "combined.info(null_counts=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:31:08.163423Z",
     "start_time": "2018-11-21T17:27:07.206081Z"
    }
   },
   "outputs": [],
   "source": [
    "combined.to_parquet(parquet_dir / 'combined.parquet')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:39:02.944907Z",
     "start_time": "2018-11-26T06:38:38.732675Z"
    },
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "combined = pd.read_parquet(parquet_dir / 'combined.parquet')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Explore data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The following figure shows the number of reviews and the average number of stars per year."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:40:38.345897Z",
     "start_time": "2018-11-26T06:39:02.946238Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7wAAAE0CAYAAAAPPz+rAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAIABJREFUeJzs3XlcVPX+x/HXAIJo6iACSoorFxUtd0RTcsncitxyqeuuuXXVm5ZeW81SW1xywS26Wt4szVIKl0zEXdQsTM0wzF3MBURkZ35/8GNyHEQwBBnez8fjPq5zzvec7+c7PvLM53w3Q2xsrAkRERERERERG2NX2AGIiIiIiIiI3A9KeEVERERERMQmKeEVERERERERm6SEV0RERERERGySEl4RERERERGxSUp4RURERERExCYp4RUpRF26dMFoNBZ2GCIiIiIiNkkJrxQLRqPR4n8uLi54eXnRoUMHFi9eTGpqamGHKPdJcnIyLVq0wGg0smnTpjuW27RpE0ajkZYtW5KcnFyAEYqIyO0yMjJYsWIFXbt2pXr16lSoUIGaNWvi7+/PyJEj+eqrryzKr1y5EqPRyPTp0wspYhF5UDkUdgAiBemVV14BID09ndOnTxMSEkJERATbtm3j888/L/B4Fi1aRGJiYoHXW5w4OTmxbNky2rZty5gxY9i9ezdubm4WZS5fvsyLL75IyZIlWbZsGU5OToUUrYiIZGRk0LdvXzZt2kTZsmXp2LEjnp6exMXFcfLkSdavX8+BAwfo0aNHYYcqIkWAEl4pViZPnmzx+fjx47Rp04YNGzawc+dOHnvssQKNp0qVKgVaX3FVt25d3nzzTSZNmsSYMWP44osvLM6PGTOGS5cuMXPmTOrUqVNIUYqICMCaNWvYtGkT9erV47vvvqNcuXIW55OSktizZ08hRSciRY2GNEux5uPjQ8uWLQE4dOhQtmV++uknBg8eTO3atXFzc8PHx4fhw4cTHR1tUa5Hjx4YjUZ++umnbO+zZcsWjEYjw4cPNx/LaQ7v9u3b6dOnDzVr1sTNzY169erx0ksvERMTY1FuyJAhGI1Gjh8/bnH8pZdeMg/RvZXJZKJGjRrUq1fP4thnn31Ghw4dqFmzJh4eHtStW5ennnqK5cuXZxvf7W4dThYREUFgYCBVqlShSpUq9OzZ847fS9awtSeffBIvLy88PDzw9/dn1qxZpKSkWJU3Go3Ur1+f2NhYJk6ciK+vL66urixcuDDH+F544QXat2/Ppk2bWLp0qfn4J598wsaNG3niiSd44YUXrK6Liopi1KhR1KtXDzc3N2rVqkX//v05fPiwVdmzZ8/y7rvv8sQTT+Dt7Y2bmxt16tRh2LBh/Pbbb1blf/vtN4xGIz169OD06dMMGzYMb29vXFxc2LJlS47tERGxVfv27QOgX79+VskuQMmSJWnTpo3588iRIxk9ejQAM2fOtJjCtGPHDgDi4uKYO3cuXbt2pU6dOri5uVGzZk369Oljru92d3vexMTEMGXKFJo0aYKnpydVqlShUaNGDB06NNtnRHayfgf88ccfzJ8/n6ZNm+Lh4YGvry9TpkwhPj4+2+tiYmKYNGkSjRo1wsPDg6pVq9KtWzfCw8Otyt76fN63bx/du3enatWqGI1GYmNj7xjbgAEDMBqN7Ny5M9vz27Ztw2g0MmTIEIvjcXFxvPPOO/j7+1OpUiUqV65Mx44d+eabb6zukZKSwpIlS+jZsyf16tXD3d2dqlWr8vTTT99xGlL9+vUxGo0kJSUxbdo0GjZsiJubG5MmTbpjW6R4Uw+vyP9zcLD+z+HLL79k1KhRODo60qlTJx5++GGio6P56quv2LhxI99++y2PPPIIkPlg/uGHH1i5ciUNGjSwulfWkOnnnnvurrHMmTOHN998ExcXFzp06ICHhwdHjhzh448/ZsOGDXz//fc8/PDDAAQEBPDVV1+xbds2fHx8zPfIeugdPXqUP//80zyMNzIykqtXr9KpUydz2TfffJO5c+fi5eXFM888Q7ly5YiJieGXX35h1apVDBgwILdfIwcPHmT27Nm0adOGYcOG8fvvvxMSEsKuXbv45ptv8PPzM5dNS0vj+eefZ+PGjdSqVYsePXrg5OTErl27mDp1KuHh4Xz11VdWfzcpKSk8/fTTXL9+nQ4dOuDo6Iinp2eOcRkMBhYuXEiLFi14/fXXad26NQ4ODrz66qtUqFCBBQsWWF2zZcsW+vfvT1paGk8++STVq1fn3LlzfPvtt2zevJkvv/yS1q1bW3zn8+fPp1WrVjRo0ABnZ2eioqJYu3YtGzduZPPmzdn2IP/555906NCBChUq0L17d5KTk7P9kSciUhyUL18egN9//z1X5bt06UJcXByhoaG0bNnSYrSWl5cXkPmCcdq0abRo0YInn3wSo9HImTNnCA0N5fvvv+fzzz+nQ4cOVve+0/Pm5s2bdOjQgVOnThEQEEDHjh0BOHfuHNu2baN169bUr18/122ePHkye/bsoVu3bpQtW5bvv/+eBQsWsHfvXkJDQy2m2hw5coRu3brx559/0rZtWzp37szVq1f57rvveOaZZ/joo4/45z//aVVHREQEs2bNokWLFvTv358LFy5gb29/x5iGDh3KunXr+OSTT7IdARccHAzAoEGDzMfOnz/PU089xe+//46/vz8DBw7k5s2bbN68mYEDB/LKK69YjLa7du0akyZNws/PjzZt2lChQgUuXrxIaGgovXv3Zs6cOQwcODDb+Pr3709kZCTt2rXDxcWFatWq3e1rlmLKEBsbayrsIETut6xe1NvfZJ44cYKAgAASEhLYtm2bRaIaHR1tfjsZGhpqkVDt2LGDZ555Bl9fX7Zv3w5kDrHy8fHB3t6eX3/9FUdHR3P5uLg4fHx8qFChApGRkdjZZQ6u6NKlC7t27bKIa9euXXTt2pUmTZqwevVqix7gVatWMWLECLp27cpnn30GwKlTp3j00Ufp3Lkz//vf/4DMB66vry9t2rQhLCyMjz/+2DzXad68ebz22mssWbKEZ599FoBq1arh7OzMgQMHKF26tMV3dOXKFVxdXe/6Ha9cudL8hv39999n2LBh5nPr1q1jwIABeHt7ExERgcFgMJd75513GDZsGDNmzDA/eDMyMhg/fjzLly9nxowZjBgxwnyvrO8jICCAzz//nFKlSt01tltt2LCBvn37Ur9+fUqUKMGPP/7IqlWrzD9Wbm13w4YNcXJyYsOGDdSqVct87pdffuGJJ57A1dWVQ4cOUaJECQAuXbpE6dKlrb7DAwcO0LVrV9q2bWv+O4LMH2DNmjUDMh/cs2fPzvHHh4hIcZCVxKSlpdGzZ086depEgwYNqF69uvn5cbusZ9DtCVWWuLg40tLSrJ5np0+fpn379pQrV479+/dbnMvpeRMaGkq/fv144YUXmDlzpsV16enpxMfH52oXhqzfAeXLlyc8PNw81Sk9PZ1//vOfhIaG8vrrr/Pvf//bfNzPz48zZ87w1VdfWSSiFy9epF27dly5coXIyEjc3d0tvhsgxwQyO/7+/vz+++8cPXqUChUqmI/HxMRQr149atSoYdFDHhgYyPbt21m6dCk9e/Y0H79+/Tpdu3bl8OHDhIeHmzsLkpOTuXz5svklfpbY2FiefPJJYmJiOHbsGM7OzuZz9evX58yZM9StW5eQkJBc/UaR4k1DmqVYmT59OtOnT2fatGkMHz6c1q1bk5CQwL/+9S+rXtmPP/6Y5ORk3n33Xavew1atWtGpUyciIyM5duwYkDnEqkePHly9epWNGzdalP/6669JSkqiT58+5mT3ThYtWoTJZGL27NlWD8s+ffrwyCOPsGHDBq5fvw5A1apVqVatGjt37iQ9PR34q3d3woQJlC5dmm3btpnvkXXu1p5JOzs7SpQokW0vd14fJDVq1LAa3hQYGIifnx9RUVHmB2NGRgaLFi3Czc2N6dOnWyR6dnZ2TJ06FYPBYDXfNsvbb7+d52QXoFOnTgwePJjDhw/z448/MmTIEKtkFzJ/IFy/fp0pU6ZYJLsA9erVo1+/fpw9e5bdu3ebj7u7u1sluwBNmjShefPmhIeHk5GRYXXe2dmZt956S8muiAjwyCOPsGTJEtzd3Vm9ejWDBw+mUaNGVKtWjd69e/PNN99gMuWtv6ZcuXLZPs+8vLwIDAwkKiqKM2fOZHttds+brGd5ds8he3v7PG85OGLECIt1Pezt7XnrrbcwGAzmF9wAmzdv5sSJEwwZMsSq17VixYq8+OKLJCUlsW7dOqs66tWrl6dkFzKnTaWkpFjEAPDpp5+Smppq0bt75MgRwsPD6dKli0WyC1C2bFkmTZqEyWRi9erV5uNOTk5WyS5kvmx4/vnniY2N5ccff8w2tv/85z9KdiVXNKRZipXb38ICvPbaa7z00ktWx7MSs927d/Pzzz9bnf/zzz+BzF66rGGq/fr1Izg4mP/97388/fTT5rJZw5n79et31xj37duHg4MDISEhhISEWJ1PSUkhPT2d6Ohoc5L++OOP89///pcff/yRpk2bEh4eTtmyZWnevDn+/v7mJDc1NZU9e/ZQp04dKlasaL7ns88+y6JFi2jWrBnPPPMM/v7++Pn54eLictd4b+fv759tUt+iRQv27dtHZGQkzZs358SJE1y5coXq1avz/vvvZ3uvrCHBt3NycsrTULHbvfPOO+ahWNOmTcu2zN69ewH4+eefs93m4uTJk0Dm339AQID5+Lfffsvy5cv5+eefuXr1KmlpaRbXXb9+3eqHUI0aNe7puxYRsVXdunWja9eu7Nixgz179nDkyBH27t3Lpk2b2LRpEx06dOCzzz6zGE11N3v37mXRokXs37+fP//802qdiAsXLlgtJnmn503Lli2pXLkyc+bM4dChQ3To0AE/Pz8effTRbF8e383t620AeHt74+7uTnR0NPHx8ZQpU8b82+Ts2bPZPpuy1hfJbt2IJk2a5Dmu3r1789Zbb/Hf//6XsWPHYjAYzGtvlCpVij59+pjLZsUWHx+fbWxXrlzJNrZjx47x0UcfsXv3bi5evGi1NeCFCxeyje1e2iPFkxJeKVayhg4nJiZy8OBBxo8fzzvvvEP16tXp3r27RdmrV68CMH/+/BzvmZCQYP5zkyZNqF27Nlu2bDHPm42Ojmbfvn34+/tTo0aNu8aYlSRll5zf6saNG+Y/BwQE8N///pfw8HCaNm3K9u3badmyJfb29gQEBLBlyxZOnjzJhQsXSEhIsOjdhcwEsEaNGnz22Wd89NFHzJ07Fzs7OwICApg6dWqeksusIVS3y5pDnNUznfX9njx58q5tze5edxrWlhu3Do269c+3unbtGpC5qFVObv37z5p7Xb58eR5//HEqV65MyZIlMRgMrF+/nmPHjmW7x6+Hh8e9NENExKaVKFGCtm3b0rZtWyBzZND69esZPXo0mzdvJjg42GLKS05CQkIYMGCAecGratWqUapUKezs7Ni5cye7du3K9t/nOz1vypQpw/fff8/MmTMJDQ01j6QqV64czz//PFOmTMnTKKScnp0xMTHmhDfr2bl+/XrWr19/x/vd+my6Wx05KVOmDH369GHp0qVs3bqVdu3asWXLFk6fPs3zzz9v8QI3K7bw8PBsF8/KLrb9+/fz9NNPk5aWRkBAAJ06daJMmTLY2dlx+PBhQkNDs/17AT07JfeU8Eqx5OzszGOPPcaaNWvw9/dn7NixtGzZ0uIfz7JlywKZCVleet/69u3LG2+8wRdffMGYMWPMczb79u2bq+vLli1LamrqHYdWZad169YYDAa2bdvGU089xYULFxg3bhyAufdx27Zt5rekt/ZIQubQqeHDhzN8+HCuXr3Knj17CAkJ4YsvvqBbt25ERESYFxG5m0uXLmV7PKtHPOt7zfr/jh07smrVqly3FfhbyW5uZcW3d+9eateufdfyycnJvP/++1SuXJmwsDCrvX6zVgrNTkG0R0SkqLOzs+OZZ57hl19+4YMPPmDbtm25TnjfffddHB0dCQsLs1jgEWDcuHHs2rUr2+ty+ve5UqVKzJkzh9mzZ/Pbb7+xa9cugoODWbBgAXFxcXd9YX6rS5cu4e3tbXU869lZpkwZ4K9n04oVKyxGkuXGvT5rhgwZwtKlSwkODqZdu3bmF8GDBw+2KJcV27Rp0xgzZkyu7v3BBx+QmJhISEgIrVq1sjg3a9YsQkND73itnp2SW5rDK8Va1apVGTt2LPHx8bzzzjsW55o2bQpgMUczN3r37o29vT2ff/45JpOJVatWUapUKbp165ar65s2bUp8fHyutzSAzHm2vr6+7N+/3zx/+PHHHwcyF3eoUKEC4eHhbN++HXt7+2yHTmUpX748Xbp0YdGiRfTo0YPLly+bh/fmxt69e7Odp5r1PWYtVPGPf/yDcuXKcfDgwWy3HypsWX//ud3r8eLFiyQkJODv72+V7MbFxfHLL7/ke4wiIsVRVvJ36zzerDUQstayuF10dDQ+Pj5WyW5GRkaennHZMRgM+Pj4MHjwYDZs2ICTkxPffvttnu6RXcIdFRXFpUuXqFGjhrnNeX025YfatWvTqlUrNm7cyIEDB9i8eTMNGjSgUaNGFuWyFmHMS2zR0dG4uLhYJbuQ/Xcici+U8EqxN2rUKFxdXVm5ciUnTpwwHx8+fDiOjo68+uqr2c6FSU9Pz7bXrmLFirRr144jR44wf/58zp49S9euXc0Pq7vJWklx3LhxnDt3zup8UlJStg+Txx9/nOTkZObPn4+np6f5oW4wGGjVqhXbtm3j4MGDNGrUyGLLm+TkZLZt22aVpJpMJvOb5ZIlS+YqdsjcRuLjjz+2OLZu3Tr27duHt7e3eVsiBwcHRowYwZ9//smECRO4efOm1b2yVposDAMGDKBs2bK8++672e7RbDKZLBYK8/T0pESJEhw8eNCiLSkpKUycONE8lFtERHK2Zs0awsLCsn15GhMTw4oVKwDLea9ZixedPXs223t6eXkRHR3N+fPnzcdMJhMzZszg119/zXOMR48e5Y8//rA6fvXqVVJTU/P03ITMBStvHdmVnp7OG2+8gclkstjOsHPnztSoUYNPPvnkjr2fWWtI5KehQ4eSnp7O888/T3p6ulXvLkCDBg1o2bIloaGhLF++PNuFxU6cOGHRTi8vL65du2b1UnjFihX88MMP+doGKb40pFmKvTJlyjBu3Dhee+013nnnHfNQHW9vbxYuXMjo0aPx9/enffv21KxZk/T0dM6dO8e+fftITk7m9OnTVvfs168fmzdvZurUqUDu9t7N0rp1a95++23eeOMNGjduzBNPPEG1atVISkrizJkz7N69Gy8vL6uN4AMCApg/fz5//vmnxSISkJkMf/311+Zyt0pMTOSZZ56hcuXKNG3alCpVqpCamsrOnTs5fPgwTZo0sZrzm5P27dvz6quvsmXLFnx9fc378Do7OzNv3jyLIUgTJ07k6NGjrFixgs2bN9O6dWsefvhhLl++zMmTJ9m7dy9Dhw419woXJDc3Nz755BMGDBhA27ZtCQgIoHbt2tjZ2XHu3DkOHDjAuXPnuHjxIvb29pQoUYKhQ4cSFBREy5Yt6dixI8nJyYSHh5t7fgvyjbyISFF14MABFi1ahIeHB82bN6dq1apA5jZ8mzdvJjExkWbNmllsf9esWTMeeugh1q5di6OjI5UrV8ZgMNC7d2+8vLwYNWoU48ePJyAggKeffhoHBwf27dvH8ePH6dixo9XuCnezbds2pkyZQtOmTfnHP/6Bu7s7MTExhIaGkpGRYZ5WlFvNmzenVatWFvvwHj16lEaNGlkMDy5RogSfffYZ3bt3p1+/fjRp0oRHH32U0qVLc+7cOSIjI4mKimL79u25noqUG126dMHT05Pz589TtmxZ81aHt1u2bBmBgYGMHTuWxYsX07RpU1xcXDh//jy//vorkZGRfPbZZ+bFwUaOHMkPP/xAp06deOaZZyhbtiyHDh1i7969BAYGZrvatEheKeEVIfPN5cKFC/nmm28YN24cjz76KAA9e/akXr16LFiwgPDwcMLCwihZsiQVK1akffv2BAYGZnu/Tp064eLiwrVr16hcuXK2Q3Vy8uKLL9K8eXMWLVrEnj172LhxIw899BCVKlWiV69eVgtsQeYqyCVKlCA1NdU8nDnLrUnu7clr6dKlmTp1Kjt27GD//v1s2LABZ2dnqlatyrRp0xg0aFCeVpxs3LgxL7/8MtOmTWPJkiUAtGnThtdee81q6ycHBwdWrFjBV199xcqVK/n++++5ceMG5cuXp0qVKowfP94qeS9I7dq1Y+fOnSxYsICtW7eyd+9eSpQogYeHB/7+/nTu3BknJydz+alTp+Lh4cHKlSsJDg7GaDTStm1bXnvtNV599dVCa4eISFHy4osv4u3tTVhYGEePHiUsLIybN2/i4uJi3k3g+eefN++BDpmLRa1cuZLp06ezdu1a88KOzZs3x8vLi0GDBuHo6EhQUBCff/45JUuWxN/fnwULFrB+/fo8J7zt2rXj7Nmz5mf09evXcXd3p1mzZowYMYI2bdrk6X7vvvsuISEhLF++nNOnT1OhQgVGjRrF5MmTLZ4zAHXr1mXXrl0EBQURGhpqnkLl4eFB7dq1zd9ffnJwcKB3797Mnj2b3r17Z7sFH2TOaw4LC2Pp0qWsW7eOr776itTUVNzd3alVqxYzZsyw2E6pffv2rFq1ig8++ICvv/4aOzs7GjduTEhICH/88YcSXskXhtjY2LxtZCYiko2sje1feeUVJk+eXNjhiIiIPPC6dOnCrl27+Pnnn8092Q+qbt26ERYWZt7eUKSo0BxeERERERG5o59++omwsDBatWqlZFeKHA1pFhERERERK0uWLOHChQusWrUKg8Gg6TlSJCnhFRERERERK/PmzePcuXNUr16dRYsWmXdaEClKNIdXREREREREbJLm8IqIiIiIiIhNUsIrIiIiIiIiNkkJr4iIiIiIiNgkJbz3QVRUlOoqAvUUZF222KaCrEttKhp12WKbCrouyV+29HdnS20BtedBp/Y8uGypLVAw7VHCKyIiIiIiIjZJCa+IiIiIiIjYJCW8IiIiIiIiYpOU8IqIiIiIiIhNUsIrIiIiIiIiNkkJr4iIiIiIiNgkJbwiIiIiIiJik3KV8O7atYs+ffpQp04djEYjK1euNJ9LTU3ljTfeoEWLFnh6euLj48PQoUM5c+aMxT2Sk5OZOHEiNWrUwNPTkz59+nDu3DmLMmfOnKF37954enpSo0YNXn75ZVJSUizK7Ny5k4CAADw8PHj00UcJDg62infZsmU88sgjeHh4EBAQwO7du/Mci4iIiBRtJlNhRyAiIoXNITeFEhISqFu3Ln379mXEiBEW527evMnPP//MhAkTqF+/PtevX+fVV1+lZ8+e7Nq1CweHzComT55MaGgoH3/8MS4uLkyZMoXevXsTHh6Ovb096enp9O7dGxcXF0JDQ7l27RojR47EZDLx/vvvA/DHH3/w7LPP8txzz7FkyRL27t3LSy+9hKurK4GBgQCsXbuWSZMm8eGHH9K8eXOWLVtGr1692Lt3L1WqVMlVLCIiUviMn+T0IrIU7Mz+fOygh+9PQFKkpGWYiEmBfxR2ICIiUqhylfB26NCBDh06ADBq1CiLc+XKleObb76xODZ79myaN2/O8ePH8fX1JS4ujk8//ZQFCxbQpk0bABYvXkz9+vXZtm0b7dq1Y+vWrRw7dozDhw9TuXJlAN566y3+9a9/8dprr1G2bFk++eQTKlasaE6AfXx8OHDgAPPnzzcnvAsWLKBfv34MGDAAgPfff58ffviB4OBg3njjjVzFIiIiIkWbCYhLNRR2GCIiUsjuyxze+Ph4AIxGIwA//fQTqamptG3b1lymcuXK+Pj4sG/fPgAiIiLw8fExJ7sA7dq1Izk5mZ9++slc5tZ7ZJU5dOgQqamppKSk8NNPP1mVadu2rbme3MQiIiIiRd/1tMKOQERECluuenjzIiUlhVdffZWOHTvy8MOZw8ouXbqEvb09rq6uFmXd3Ny4dOmSuYybm5vFeVdXV+zt7S3KPP7441b3SEtL48qVK5hMJtLT063uc3s9d4slO1FRUbn8Bu6t/N9hi3WpTaqrsOopyLrUprsp9QDEkPd7ent753v9D6oPP/yQt99+m2HDhplHX2Xnhx9+YMaMGRw7dgxHR0f8/Px4++23qVWr1n2NL9VkIC3DhIOdenpFRIqrfE1409LSGD58OHFxcXz++ed3LW8ymTAY/noI3frnW+VUxvT/K1IYDAaLP+dUT25iuV1efsBERUUV2A8eW6xLbVJdhVVPQdalNuXCHebo3k1+t7Ug/66Kkv3797N8+XJ8fX1zLPfHH3/Qr18/XnjhBRYvXsyNGzd444036NWrF4cOHbqvMdoBN1IzMDppfQ4RkeIq34Y0p6WlMWTIEI4cOcK6desoX768+Zy7uzvp6elcuXLF4prLly+be2Pd3d2telivXLli0WObXZnLly/j4OBA+fLlrXqE71TP3WIRERGRO4uLi2PYsGHMmzfPPH3pTn7++Wfzjg41atTgkUceYfz48Zw8edLqWZzfShhMXEvOuK91iIjIgy1fEt7U1FQGDRrEkSNHCAkJwcPDw+J8gwYNKFGiBGFhYeZj586d4/jx4/j5+QHQrFkzjh8/brE9UFhYGE5OTjRo0MBcZtu2bRb3DgsLo2HDhpQoUQJHR0caNGhgUU9Wmax6chOLiIiI3Nm4ceMIDAwkICDgrmWznrsrVqwgPT2d+Ph4Pv/8cxo1amQ1vSi/2QE307Q3kYhIcZarIc03btwgOjoagIyMDM6ePUtkZCQuLi5UqlSJAQMGcOjQIT7//HMMBgMxMTEAlC1bFmdnZ8qVK8c///lPXn/9ddzc3MxbAfn6+prn5LZt25Y6deowYsQIpk2bxrVr13j99dfp378/ZcuWBWDQoEEsXbqUSZMmMWjQIPbt28f//vc/li1bZo519OjRvPDCCzRu3Bg/Pz+Cg4O5ePEigwYNAshVLCIiUrzcyxZIxXX7o+XLlxMdHc3ixYtzVb5q1ap8/fXXDBw4kAkTJpCRkcEjjzzCmjVr7nNIZIaxAAAgAElEQVSkmRLTlfCKiBRnuUp4Dx06xFNPPWX+PH36dKZPn07fvn2ZNGkSoaGhAFYJ44IFC3juuecAePfdd7G3t2fQoEEkJSXRunVrFi1aZN731t7eni+++IIJEybQsWNHSpYsSc+ePZk2bZr5ftWqVePLL7/kP//5D8HBwVSsWJGZM2eatyQC6N69O1evXuX9998nJiaGOnXq8OWXX+Ll5WUuc7dYRERExFpUVBRTp05lw4YNODo65uqamJgYXnzxRfr06UOPHj24ceMG7777LgMHDiQkJAQ7uzsPNvs7C5ClZQAYiD51hoeumbC3gXWrCnKhu4Kg9jzY1J4Hly21Bf5+e+62zkauEt5WrVoRGxt7x/M5nctSsmRJ3n///RxXcaxSpQpffPFFjvd57LHH2L59e45lhg4dytChQ/9WLCIiImIpIiKCK1eu4O/vbz6Wnp7O7t27CQ4O5vz58zg5OVlcs3TpUkqVKsXUqVPNx5YsWYKvry/79u2zuNft/s5iYakZJg4f/J3KlSvjWb4EZR2L9kttW1s8Te15sKk9Dy5bagsUTHvyfVsiERERsU1dunShYcOGFsdGjx5NzZo1+fe//51tr29iYqLVCKqszxkZ939BKUc7A9eSM4p8wisiIvdGCa+IiIjkitFotFqVuVSpUri4uFC3bl0A3nrrLQ4ePMj69esB6NChAwsXLmTGjBn06tWL+Ph43n77bSpXrmxelPJ+crAzkJCqebwiIsVVvm1LJCIiInLx4kVOnjxp/hwQEMCyZcsIDQ2ldevW9OjRAwcHB9asWUPp0qULJCYtXCUiUnyph1dERETu2XfffWfxOSgoyKpMjx496NGjR0GFZCU53USGyYSdwQZWrhIRkTxRD6+IiIjYNBPaj1dEpLhSwisiIiI2zcnOwLWk+79AloiIPHiU8IqIiIhNc7AzcCNVCa+ISHGkhFdERERsnhauEhEpnpTwioiIiM3LWrhKRESKFyW8IiIiYvMygCT18oqIFDtKeEVERMTmOdoZiE3WPF4RkeJGCa+IiIjYvBJ2BuJT1MMrIlLcOBR2ACIiUrQYPzl3hzOlYGf252IHPXz/AhLJJS1cJSJS/KiHV0RERIqFpDQTJi1cJSJSrCjhFRERkWIhc+Gqwo5CREQKkhJeERERKRYcDBCnhatERIoVJbwiIiJSLDjaG4hLUcIrIlKcKOEVERGRYiMpQ3N4RUSKEyW8IiIiUmxo4SoRkeJFCa+IiIgUG+kmSNbCVSIixYYSXhERESk27A1wPVXzeEVEigslvCIiIlJsONkbiNVKzSIixYYSXhERESlWEtM1h1dEpLhQwisiIiLFSlKaEl4RkeJCCa+IiIgUK5kLVynpFREpDpTwioiISLFib4D4FM3jFREpDnKV8O7atYs+ffpQp04djEYjK1eutDhvMpmYPn06tWvXpmLFinTp0oVjx45ZlImNjWX48OF4eXnh5eXF8OHDiY2NtShz5MgROnfuTMWKFalTpw4zZ8602itv3bp1+Pn54e7ujp+fHyEhIfclFhEREbFNjnZwTQmviEixkKuENyEhgbp16zJjxgycnZ2tzs+dO5cFCxYwc+ZMtm7dipubG926dSM+Pt5cZujQoURGRrJ69WrWrFlDZGQkL7zwgvn89evX6datG+7u7mzdupUZM2Ywb9485s+fby4TERHB4MGD6dWrFzt27KBXr14MHDiQAwcO5GssIiIiYrsMBgOJmscrIlIs5Crh7dChA6+//jqBgYHY2VleYjKZCAoKYty4cQQGBlK3bl2CgoK4ceMGa9asAeD48eNs2bKFOXPm4OfnR7NmzZg9ezabNm0iKioKgNWrV5OYmEhQUBB169YlMDCQsWPHsnDhQnMvb1BQEK1atWLChAn4+PgwYcIEHnvsMYKCgvI1FhEREbm7Dz/8EKPRyMSJE3MsZzKZWLhwIU2bNsXd3R0fHx/efPPNggnyDpI0h1dEpFj423N4T506RUxMDG3btjUfc3Z2pkWLFuzbtw/I7Jl96KGH8PPzM5dp3rw5pUuXtijj7+9v0YPcrl07Lly4wKlTpwDYv3+/RT1ZZbLukV+xiIiISM7279/P8uXL8fX1vWvZKVOm8PHHH/Pmm28SERHBl19+SYsWLQogyjtLy4AUJb0iIjbP4e/eICYmBgA3NzeL425ubly4cAGAS5cu4erqisFgMJ83GAxUqFCBS5cumct4enpa3SPrXLVq1YiJicm2nqx75Fcs2clr729B9hbbYl1qk+oqrHoKsq6i26ZSBVR/3uspyLruVI+3t/c91F+0xMXFMWzYMObNm8d7772XY9moqCiWLFnCrl278PHxKaAI784OuJGaQXl7+8IORURE7qO/nfBmuTWBhMzhS7cnlbe7W5msocx3K3P7sfyI5XZ5+QETFRVVYD94bLEutUl1FVY9BVlXkW7TznN5vuSe6r+HegqyruKQ2N5J1tShgICAuya8oaGhVKtWjS1btvDss8+SkZFBy5Ytefvtt61eUBckJ3u4mpxB+ZJKeEVEbNnfHtLs4eEBYNU7evnyZfODzN3dncuXL1usuGwymbhy5YpFmezuAX/12Hp4eORYT37FIiIiItlbvnw50dHRTJkyJVfl//jjD86cOcPatWtZuHAhixcvJioqij59+pCRUXgrJRsMBhI1pFlExOb97R7eqlWr4uHhQVhYGI0aNQIgKSmJPXv2MHXqVACaNWvGjRs3iIiIMM+djYiIICEhwfy5WbNmvPnmmyQlJVGyZEkAwsLCqFSpElWrVgWgadOmhIWF8a9//ctcf1hYmPke+RWLiIiIWIuKimLq1Kls2LABR0fHXF2TkZFBcnIyixcvplatWgAsXryYJk2a8OOPP9KkSZMc67tXaRkABk6fOZ1jmZJXik7Sa2uLa6o9Dza158FlS22Bv9+eu424ylXCe+PGDaKjo4HMB9fZs2eJjIzExcWFKlWqMHLkSD788EO8vb2pVasWH3zwAaVLl6Znz54A+Pj40L59e8aPH8/cuXMxmUyMHz+eJ5980hxgz549mTlzJqNGjWLChAmcOHGCOXPm8PLLL5uHGo8YMYLOnTsza9YsunbtyrfffsuOHTvYuHEjkPm2Nj9iEREREWsRERFcuXIFf39/87H09HR2795NcHAw58+fx8nJyeIaDw8PHBwczMkuQM2aNXFwcODs2bM5Jrx/57mcmmHi8MHf8aridccyN9NMVHN3pITdnac0PSgKchpEQVB7Hmxqz4PLltoCBdOeXCW8hw4d4qmnnjJ/nj59OtOnT6dv374EBQUxduxYEhMTmThxIrGxsTRu3Ji1a9dSpkwZ8zVLly7llVdeoXv37gB06tTJYt5PuXLl+Prrr5kwYQJt2rTBaDQyevRoxowZYy7j5+dHcHAw06ZNY/r06VSvXp3g4GCLh2V+xCIiIiLWunTpQsOGDS2OjR49mpo1a/Lvf/87217f5s2bk5aWxsmTJ6levTqQOcw5LS2NKlWqFEjcd2IAElIzMDppHq+IiK3KVcLbqlUrYmNj73jeYDAwefJkJk+efMcyLi4uLFmyJMd6fH192bBhQ45lAgMDCQwMvO+xiIiIiCWj0YjRaLQ4VqpUKVxcXKhbty4Ab731FgcPHmT9+vUAPP744zz66KOMHj2a6dOnAzB58mSaNGlilTwXtKyFq5TwiojYrr+9aJWIiIhIlosXL3Ly5EnzZzs7O7744gvc3Nzo0qULPXr04OGHH+Z///sfdnaF+zPEzmDgZlrRmcMrIiJ5l2/bEomIiEjx891331l8DgoKsipTsWJFli9fXlAh5UmiEl4REZumhFdExAYYP8lpH9lS2e4zGzvo4fsXkEgRkWqCtAwTDkVg4SoREck7DWkWERGR4suUuXCViIjYJiW8IiIiUmw52cO1ZCW8IiK2SgmviIiIFFv2BgMJmscrImKzlPCKiIhIsZaYroRXRMRWKeEVERGRYi0lHdIzlPSKiNgiJbwiIiJSrJlMJhLSNI9XRMQWKeEVERGRYs3J3qCFq0REbJQSXhERESnWHOwMJKRqSLOIiC1SwisiIiLFnhauEhGxTUp4RUREpNhLTjeRYVLSKyJia5TwioiISLFnAhK1H6+IiM1RwisiIiLFnpOdFq4SEbFFSnhFRESk2HOwM3BDC1eJiNgcJbwiIiIiaOEqERFbpIRXREREBEhOy8CkhatERGyKEl4RERERIB318oqI2BolvCIiImJzMkwmohLsCDuflOtrHO0MxGrhKhERm+JQ2AGIiIiI5JfY5Axm/nSdb/5I5MJNJ4znbtCqohMOdoa7XlvCzkB8iglKF0CgIiJSINTDKyIiIjbD2cHAyhM3uXAzs6c2NsXEz1dSc329hjSLiNgWJbwiIiJiM5zsDXTxcrY4FnY+OdfXJ6WZtHCViIgNUcIrIiIiNqV7dcuEd/uFZNIycpfEZgBJ6fchKBERKRRKeEVERMSmBFRywuj415zd66kmDl7O3bBmBwPEaeEqERGbkS8Jb3p6OtOmTeORRx7Bw8ODRx55hGnTppGWlmYuYzKZmD59OrVr16ZixYp06dKFY8eOWdwnNjaW4cOH4+XlhZeXF8OHDyc2NtaizJEjR+jcuTMVK1akTp06zJw502ro0bp16/Dz88Pd3R0/Pz9CQkIszucmFhERESmaHO0NdKl6+7Dm3K3W7GhvIC5FCa+IiK3Il4R3zpw5LFu2jJkzZxIREcGMGTNYunQps2bNMpeZO3cuCxYsYObMmWzduhU3Nze6detGfHy8uczQoUOJjIxk9erVrFmzhsjISF544QXz+evXr9OtWzfc3d3ZunUrM2bMYN68ecyfP99cJiIigsGDB9OrVy927NhBr169GDhwIAcOHMhTLCIiIlJ0PVOtpMXnHRdSSMnlglRJuRz+LCIiD758SXgjIiLo2LEjnTp1omrVqnTu3JlOnTpx8OBBILNHNSgoiHHjxhEYGEjdunUJCgrixo0brFmzBoDjx4+zZcsW5syZg5+fH82aNWP27Nls2rSJqKgoAFavXk1iYiJBQUHUrVuXwMBAxo4dy8KFC829vEFBQbRq1YoJEybg4+PDhAkTeOyxxwgKCsp1LCIiInJ3H374IUajkYkTJ+aq/O+//07lypV5+OGH73Nk8FhFJ8rY/5W4JqSZOHA5JVfXauEqERHbkS8Jb/Pmzdm5cye//fYbAL/++is7duzgiSeeAODUqVPExMTQtm1b8zXOzs60aNGCffv2AZlJ80MPPYSfn5/FfUuXLm1Rxt/fH2fnv4YptWvXjgsXLnDq1CkA9u/fb1FPVpmse+QmFhEREcnZ/v37Wb58Ob6+vrkqn5KSwuDBg2nRosV9jiyTg52BRuUsV58KO5e71ZrTTZCshatERGyCQ37cZNy4cdy4cQM/Pz/s7e1JS0tjwoQJDB06FICYmBgA3NzcLK5zc3PjwoULAFy6dAlXV1cMhr8WmTAYDFSoUIFLly6Zy3h6elrdI+tctWrViImJybaerHvkJpbsZPUy51Zey/8dtliX2qS6Cquegqwrf+spVYD1F1Rdea+nIOu6Uz3e3t73UH/REhcXx7Bhw5g3bx7vvfderq5544038PX1pWXLluzates+R5ipSdl0wq/+9VNnV0wKyekmnOwNOVwF9ga4nppBSQf7+x2iiIjcZ/mS8K5du5ZVq1axbNkyateuzeHDh5k0aRJeXl7079/fXO7WZBYyhxffnuDe7m5lsoYc3a3M7cdyU+ZWefkBExUVVWA/eGyxLrVJdRVWPQVZV77Xs/Ncni+55/oLqq57qKcg6yoOie2dZE0LCggIyFXCu2nTJjZt2kR4eDjr168vgAgzeZfOoLyTHVf/f9Xlm2kmIi6l0KqSU47XOdpBbHIG7s5KeEVEirp8SXhff/11xowZQ48ePQDw9fXlzJkzzJ49m/79++Ph4QFk9sJWrlzZfN3ly5fNPa3u7u5cvnzZIvE0mUxcuXLFokxWT+2t94C/emw9PDyyLXPr+bvFIiIiItlbvnw50dHRLF68OFflL168yNixY/n0008pU6ZMnur6O6Mg0jLAzmCgUZkUtiT/9XPn2xNXqZp29y2K7ADDQw/WPN6CHOlSENSeB5va8+CypbbA32/P3V5A50vCe/PmTeztLd+C2tvbk5GR+Ua1atWqeHh4EBYWRqNGjQBISkpiz549TJ06FYBmzZpx48YNIiIizPN4IyIiSEhIMH9u1qwZb775JklJSZQsmbn6YlhYGJUqVaJq1aoANG3alLCwMP71r3+ZYwkLCzPfIzexiIiIiLWoqCimTp3Khg0bcHR0zNU1w4cPZ/DgwTRt2jTP9f2dXvTUDBOHD/7O0/+owJbLf21x+HO8A26VKuLskPOw5uR0E94eOfcEF6SCHOlSENSeB5va8+CypbZAwbQnXxat6tixI3PmzGHTpk2cOnWKkJAQFixYQNeuXYHM4cMjR45kzpw5rF+/nqNHjzJq1ChKly5Nz549AfDx8aF9+/aMHz+e/fv3ExERwfjx43nyySfNX0LPnj1xdnZm1KhRHD16lPXr1zNnzhxGjRpl7hUeMWIE27dvZ9asWfz222/MmjWLHTt2MHLkyFzHIiIiItYiIiK4cuUK/v7+uLq64urqyq5du1i2bBmurq4kJ1svCrV9+3ZmzpxpLv/iiy+SkJCAq6sr//3vf+97zPXKO1Ch5F8/d5LSYd+lu6/WnLlw1YPVwysiInmXLz287733Hu+88w4vvfQSly9fxsPDgwEDBvDyyy+by4wdO5bExEQmTpxIbGwsjRs3Zu3atRbDm5YuXcorr7xC9+7dAejUqZPF3KBy5crx9ddfM2HCBNq0aYPRaGT06NGMGTPGXMbPz4/g4GCmTZvG9OnTqV69OsHBwTRp0iRPsYiIiIilLl260LBhQ4tjo0ePpmbNmvz73//Ottd39+7dFp9DQ0P58MMP+eGHH6wWorwf7AwG2ng6sTo60Xxs6/kkHvfMuffW3gDxKRk4aR6viEiRli8Jb5kyZZgxYwYzZsy4YxmDwcDkyZOZPHnyHcu4uLiwZMmSHOvy9fVlw4YNOZYJDAwkMDDwb8UiIiIiloxGI0aj0eJYqVKlcHFxoW7dugC89dZbHDx40Lw4VdbxLIcOHcLOzs7q+P10e8K7NyaFm2kZlHK480A3Rzu4lpJBBSW8IiJFWr4MaRYRERGBzEWqTp48WdhhWKhjdMDD+a+fPCkZsPtizsOaDQYDiWka0iwiUtTlSw+viIiIFE/fffedxeegoKAcyz/33HM899xz9zMkK4b/H9a86ve/ennDzifTvnLJHK9L0hxeEZEiTz28IiIiYvPa3DZnN+LPFOJTM3K8Ji0DUpT0iogUaUp4RURExOb9o5wDnqX++tmTmothzXbAjbskxSIi8mBTwisiIiI2z2Aw0NbTcghz2HnrbZRu5WQPV5OV8IqIFGVKeEVERGxcYmIily9ftjh2+fJlZs2axeuvv87BgwcLKbKC1eZhy2HN+/9M4XrKnRNag8FAooY0i4gUaVq0SkRExMaNHz+eY8eOER4eDkBCQgLt2rXj9OnTQOZCUyEhITRv3rwww7zvapSxx+she07fSAcg3QQ7LibTxcv5jtdopWYRkaJNPbwiIiI2bu/evXTq1Mn8ec2aNZw+fZo1a9Zw/PhxfHx8+OCDDwoxwoKRtVrzrcLO5TysOS0DUjOU9IqIFFVKeEVERGxcTEwMDz/8sPnzhg0baNasGe3atcPd3Z3nnnuOyMjIQoyw4Nye8P54JZXYHObpGoAELVwlIlJkKeEVERGxcaVLlyY2NhaAtLQ0du/ezeOPP24+7+zsTHx8fCFFV7CqlXGgehl78+cME2y/cOdeXi1cJSJStCnhFRERsXENGzbk008/5eeff+aDDz7gxo0bdOzY0Xz+5MmTuLu7F2KEBctqWHMOqzXbGQzc1DxeEZEiS4tWiYjcR8ZPzt3hTCnYmf252EEPZ3tc5F5NmTKFbt260aZNG0wmE08//TQNGzY0n//222/x8/MrxAgLVltPJ4KP3zR//vlKKleSMnAtmX0/gBauEhEpupTwioiI2LgGDRpw4MAB9u3bR5kyZWjVqpX5XGxsLEOHDqVly5aFGGHBqvyQA95lHYi6ngZABhB+IZnu1bNfrTnVBGkZJhzsDAUYpYiI5AclvCIiIjYsKSmJuXPn0rRpUzp37mx13mg0MnLkyEKIrHA97ulkTngBws4n3THhxZS5cFU5J/vsz4uIyANLc3hFRERsWMmSJZk9ezZnz54t7FAeKLfP4z18NY1LienZlnWyh2tauEpEpEhSwisiImLj6tevT3R0dGGH8UDxLG1PbaPlQLfwO6zWbG8wkKB5vCIiRZISXhERERv3+uuvs2LFCjZt2lTYoTxQ8rJac2K6El4RkaJIc3hFRERs3EcffYTRaKRv3754enpSrVo1nJ0t56saDAa+/PLLQoqwcLTxdCLoaIL589FraVy8mU7FUtZzdVPSIT3DhL0WrhIRKVKU8IqIiNi4X3/9FYPBQOXKlQE4ffq0VRmDofglcu7O9tRzceCXa38tXrXtfDJ9apWyKmsymbiZlkEZRy1cJSJSlCjhFRERsXGHDx8u7BAeWG08nSwS3q13SHid7A3EJpso41iQ0YmIyN+lObwiIiJSbAV4OnFr3/ZvcWmcTbBerdnBzsCNVK3ULCJS1KiHV0REpBiJj4/n+vXrZGRYJ29VqlQphIgKV4WS9jxSvgQ/X001Hws/n8xz3ta9vDe1cJWISJGjhFdERKQYWLFiBR999FGO2xNdvXq1ACN6cLR52Mki4d16PinbhDc53USGyYRdMZzvLCJSVGlIs4iIiI379NNPGTt2LFWqVOHVV1/FZDIxcuRIxo8fj7u7O/Xr12fevHmFHWahaV3JyeIH0e/X0zl9I82qnAlI1H68IiJFihJeERERGxcUFESrVq34+uuvGThwIAAdOnTgtddeY+/evcTGxnL9+vXCDbIQlXeyo0GFEhbHstuT18nOwLVkzeMVESlKlPCKiIjYuOjoaLp27QqAnV3moz81NXMIr9FopH///ixbtqzQ4nsQtPV0svgcds464c1cuEo9vCIiRUm+JbwXL15kxIgR1KxZEw8PD/z8/Ni5c6f5vMlkYvr06dSuXZuKFSvSpUsXjh07ZnGP2NhYhg8fjpeXF15eXgwfPpzY2FiLMkeOHKFz585UrFiROnXqMHPmTEwmy4fPunXr8PPzw93dHT8/P0JCQizO5yYWERERW1G6dGnzs/Khhx7C3t6eixcvms+XL1+e8+fP5/m+H374IUajkYkTJ96xzI4dO+jbty8+Pj5UqlSJFi1a8Omnn+a9EffZYxWdsLtlau4fN9I5GW89rDlRC1eJiBQp+ZLwxsbG8uSTT2Iymfjyyy/Zt28f7733Hm5ubuYyc+fOZcGCBcycOZOtW7fi5uZGt27diI+PN5cZOnQokZGRrF69mjVr1hAZGckLL7xgPn/9+nW6deuGu7s7W7duZcaMGcybN4/58+eby0RERDB48GB69erFjh076NWrFwMHDuTAgQN5ikVERMRWeHt7c/ToUQAcHByoX78+q1atIjU1laSkJL744guqVq2ap3vu37+f5cuX4+vrm2O5iIgIfH19Wb58OXv27GHIkCGMGzeO1atX33N77gejkx2Nbx/WnE0vb3JahtWLdhEReXDlyyrNH330ERUrVmTx4sXmY9WqVTP/2WQyERQUxLhx4wgMDAQy5xN5e3uzZs0aBg0axPHjx9myZQsbN27Ez88PgNmzZ9OpUyeioqLw9vZm9erVJCYmEhQUhLOzM3Xr1uW3335j4cKFjBkzBoPBYJ6nNGHCBAB8fHzYsWMHQUFBfPzxx7mKRURExJZ06dKFoKAgkpKSKFmyJBMmTOCf//wn1apVw2AwkJCQwKJFi3J9v7i4OIYNG8a8efN47733ciz70ksvWXweMmQIO3bsYP369fTq1eue2nO/tPUsyf4//1qtOex8MoN8SmG4ZVXmdDJ7eUs5aKVmEZGiIF96eL/77jsaN27MoEGDqFWrFo899hhLliwxvwE9deoUMTExtG3b1nyNs7MzLVq0YN++fUDmG+CHHnrInOwCNG/enNKlS1uU8ff3x9nZ2VymXbt2XLhwgVOnTgGZb5xvrSerTNY9chOLiIiILXnxxRc5evQoJUuWBDIT4NDQUPr378/AgQMJCQmhd+/eub5f1kvjgICAe4onPj4eo9F4T9feT49VdOTWPPZMQjonrqdblHG0MxCrhatERIqMfOnh/eOPP/j4448ZNWoU48aN4/Dhw7zyyisADB8+nJiYGACLIc5Zny9cuADApUuXcHV1tXiLajAYqFChApcuXTKX8fT0tLpH1rlq1aoRExOTbT1Z98hNLNmJiorKxTdx7+X/DlusS21SXYVVT/7XZb2X5/2pv6DqKci68l5PQdZ1p3q8vb3vof6C17x5c5o3b57n65YvX050dLTFqK682LhxI+Hh4WzatOmuZf/Of4tpGQAGTp85nafr6pVx5Kfr9ubP6369xLOelnN5r9pBQqmCH9ZckP8OFgS158Gm9jy4bKkt8Pfbc7fnbr4kvBkZGTRs2JA33ngDgEcffZTo6GiWLVvG8OHDzeUMt23UbjKZrBLc292tTFYv8t3K3H4sN2VulZcfMFlDsAuCLdalNqmuwqrnvtS181yeL7mn+guqnoKs6x7qKci6ikpiC5mLUi1evPiOQ4jXrl3L0KFDuXr1ao73iYqKYurUqWzYsAFHR8c8x7F3716GDRvGzJkzady48V3L/53vODXDxOGDv+NVxStP13U2JPHTob/W9Dh4w4mXKley+o3gXSHv7f87CvLfwYKg9jzY1J4Hly21BQqmPfkypNnDwwMfHx+LY//4xz84e/as+TKDiZQAACAASURBVDxg7mXNcvnyZXNPq7u7O5cvX7ZYCMJkMnHlyhWLMtndA/7qsfXw8MixntzEIiIiYkvutshSRkZGji99s0RERHDlyhX8/f1xdXXF1dWVXbt2sWzZMlxdXUlOtl7kKcuePXvo1asXkydPZsiQIXluQ0Fp6eFIiVt+HV24mcHxOMse3qQ0kxauEhEpIvIl4W3evDknTpywOHbixAmqVKkCQNWqVfHw8CAsLMx8PikpiT179pjn7DZr1owbN24QERFhLhMREUFCQoJFmT179pCUlGQuExYWRqVKlcyrSzZt2tSinqwyWffITSwiIiK2JqeE9sCBA7maU9ulSxd2797Njh07zP9r2LAhPXr0YMeOHXfs9d21axe9evXi5ZdfZtSoUffchoJQuoQdfu6W7Qg7b5nIZwBJllN7RUTkAZUvQ5pHjRpFhw4d+OCDD+jevTuRkZEsWbKE1157Dch8yI4cOZIPP/wQb29vatWqxQcffEDp0qXp2bMnkLmacvv27Rk/fjxz587FZDIxfvx4nnzySXM3d8+ePZk5cyajRo1iwoQJnDhxgjlz5vDyyy+bH+QjRoygc+fOzJo1i65du/Ltt9+yY8cONm7cmOtYREREirqgoCCLlZcnT57M22+/bVUuLi6O69ev52rRKqPRaJUYlypVChcXF+rWrQvAW2+9xcGDB1m/fj2QuQ9v7969GTJkCM8++6x5LQ17e3sqVKhwz+27n9p4OrHzYor587bzyYyoU9r8W8PBAHHJGTg72N/pFiIi8oDIl4S3UaNGrFy5kqlTp/L+++9TuXJl/vOf/zB06FBzmbFjx5KYmMjEiROJjY2lcePGrF27ljJlypjLLF26lFdeeYXu3bsD0KlTJ4vtDsqVK8fXX3/NhAkTaNOmDUajkdGjRzNmzBhzGT8/P4KDg5k27f/Yu/O4qMr9geOfMwv7KrJoCi4guKWmgppmZGnuWpql93fVMpe0xbLUulaa5XLTNEtyyfZNDa9ombeUXEpBc03N0OtaCqLsss3M+f0xMjICijIzLH7frxfBnHM43+cLxjnfeZ7zPDOZNWsWDRs2ZMWKFbRr1+6m2iKEEEJUZ35+fpY3jE+fPk1gYCBBQUFWxyiKgpubG23atLGac6Mizp8/z4kTJyyvv/zySy5fvsyiRYtYtGiRZXv9+vU5ePCgTWLaWsdAZ5w1WRRNxpyca+JwmoHmtczr9DppFTIKTAS5S8ErhBBVnU0KXoAePXrQo0ePMvcrisLUqVOZOnVqmcf4+vqydOnS68Zp3rw5GzZsuO4x/fv3t6yxe6ttEUIIIaqzRx55hEceeQSAPn368OKLL97yMkLX891331m9jomJKfH62m1VnZtOoWOgMz+fuzqUefPf+ZaCFyDPJM/wCiFEdWCTZ3iFEEIIUXVNmTKFI0eOWG1btWoV7dq1IzQ0lMmTJ2MyydqyxUXXdbZ6veVcPqZiE1XJxFVCCFE9SMErhBBC1HBz584lISHB8vrPP//kqaeeQqPR0KZNG5YtW2b1vG9NoFj+c2uiApxwKTZiOTXPxO+XCi2vjSrky8RVQghR5UnBK4QQQtRwf/zxh9W6tytXrsTV1ZWffvqJVatWMWTIED7//PNKbKHt6TQK3now3OLQYxedwt1B1r28m4vN1qxVILNQesWFEKKqk4JXCCGEqOEyMzOtZlfetGkT0dHReHl5AdCxY0dOnz5dWc2zmwauKqDc8tDj0oY1G6+cy0kD6flS8AohRFUnBa8QQghRwwUGBnL06FEAzp07x4EDB7jvvvss+zMzM9Fqa96MwxoFWvrpb3nocaS/E+66q+Oi0/JV9l80D2tWFIVcozzDK4QQVZ3NZmkWQgghRNXUt29fli1bRn5+Pnv27MHZ2ZmePXta9v/+++80aNCg8hpoR85ahXAfHX+kG3DV3dxDvU5ahc5BTmw8e3Uoc/xf+dxV2wkwT1wlhBCiapMeXiGEEKKGmzp1Kv369WPlypUkJyfz3nvvERAQAJh7d9etW0d0dHQlt9J+fF201HXXkH8LPbKlDWsuei7YPHGVFL1CCFGVSQ+vEEIIUcO5u7uXuc69h4cHhw8fxs3NzcGtcqwQTz1ZBQXkGVV0mvL39Lb1d8JTr5BVaC5sMwtV9qYW0j7ACa0CWQUmnF1r3nBwIYSoKaSHVwghhLiNaTQavL290ev1ld0Uu2vqq0eFm5rESq9R6HLNbM3xV2ZrdtJAWoFMXCWEEFWZFLxCCCGEuC1oNQrNffU3PdnUtcOat57Pp9Ckmieukud4hRCiSpOCVwghhBC3DTe9hjDvmyt629TW4+10dRh0dqHK7gsFAOTJM7xCCFGlScErhBBCiNuKv6uWAJfyT2Kl0yjcU6f0Yc0GExRI0SuEEFWWFLxCCCGEuO008tLholUwlvN53vuuGda8/XwB+UYVrQIX825xoV8hhBB2J7M0CyFuOz4f/XWdvW6wveT+9JF32K9BQgiHUxSF5rX0/HahAG05Jlm+00+Pr7NCWr65QL5sUNl1oYDOQc6czjYS6KZFo9zcOr9CCCHsT3p4hRBCCHFb0mkUmvnquFyOIclaRaFrGcOaNQqcyjLYpY1CCCEqRgpeIYQQQty2PJ20NPTQkWu48fJC1w5r/uV8PnkGFb1G4dxlozzLK4QQVZAUvEIIIYS4rdVx11LLRUuh6foFa4taemq7XL11yjPCzhTzbM0uWoWkjEK7tlMIIcTNk4JXCCGEELe9Jt46tIqC6TqTWGkUhXtLDGvOs+zLKFDJLJAJrIQQoiqRglcIIYQQtz1FUWhRS0/BDUY2R99hXfDuTC7gssFcJLvpFI5lSsErhBBViRS8QgghhBCAk1YhwkdnKWBL08xHR6Dr1dunfBPsSM63vC4wqpzPkaJXCCGqCil4hRBCCCGu8HHWUt9dS14ZE1ApisK9dUufrRnMz/KeyjFcd2i0EEIIx5GCVwghhBCimPqeOrydNBjKmMQq+pqCNyGlgJzCq2OhtcBJWaZICCGqBCl4hRBCCCGuEe6jQ0VBLaWnNtxbR123q7dQhSb46a+rvbw6jcL5yyZZpkgIIaoAKXiFEEIIIa6hURRa1tKRX0rRqihKiV7eZX/kkJp39dldFy2yTJEQQlQBUvAKIYQQ4pbNmzcPHx8fXnzxxesed+jQIXr16kVQUBBNmzZlzpw5pfaeViUuOg1NfPTkljKJVZ8QV5yK3UVlF6q8cyDbkpMsUySEEFWDXQre0i5+qqoya9YsIiIiCAoKonfv3hw5csTq+9LT0xk9ejTBwcEEBwczevRo0tPTrY4pzwVz7dq1REVFERAQQFRUFOvWrbPaX562CCGEEOL6du3axSeffELz5s2ve1xmZiYDBw4kICCAzZs3M3v2bBYtWsR7773noJbeulouWoLcNCV6euu4aRkV4W617ZfkAjYVG9rsplNIypCCVwghKpPNC96yLn4LFy7k/fffZ86cOWzevBl/f38GDhxIVlaW5ZhRo0Zx4MABVq1axerVqzlw4ABjxoyx7C/PBTMxMZHHH3+cwYMHs23bNgYPHsyIESPYvXv3TbVFCCGEEGXLyMjgySefZNGiRfj4+Fz32FWrVpGbm0tMTAzNmjWjf//+PPvssyxevLjK9/ICNPDU4aZTMF7T1ocbudLcV2e17d3fs7mUf3UCq0KTLFMkhBCVyaYFb1kXP1VViYmJ4bnnnqN///40a9aMmJgYsrOzWb16NQBHjx7lp59+YsGCBURFRREZGck777zDxo0bSUpKAsp3wYyJiaFLly5MmjSJ8PBwJk2aROfOnYmJiSl3W4QQQghxfUXX0a5du97w2MTERDp27Iirq6tlW7du3Th37hynTp2yZzNtQlEUmvrqufZxXq2i8FIrT/TF7qYyC1UWHMiy3JcULVNkLGPGZyGEEPalu/Eh5Vf84jd37lzL9lOnTpGcnMx9991n2ebq6kqnTp1ISEhg5MiRJCYm4uHhQVRUlOWYDh064O7uTkJCAmFhYWVeMN98801OnTpFgwYN2LVrF6NHj7ZqV7du3Vi6dGm52yKEEEKIsn3yySf873//Y8mSJeU6PiUlhbp161pt8/f3t+xr0KBBqd9X9IZ3RdjiHEVcDHA0R8FZe3WbAgwM1LHynN6ybev5AlYf/JsoX3PPrkGFnPMq9V2pEFvmUhVIPlWb5FN11aRcoOL5hIWFXXe/zQre6138kpOTgasXtyL+/v6cO3cOMF/w/Pz8UBTFsl9RFGrXrk1KSorlmBtdMJOTk0uNU3SO8rSlNDf7i3DkP8SaGEtyklj2jePmwPiOiiU5OTpWWXFudOGt7pKSkpgxYwYbNmzAycmp3N9X/PoOWHpAr91eXEV/lklJSTb/fQReNvK/LAOu2qvtHn2HyoFf0vkj/erau5+fc+aB8Fr4OJu7fy8bVIL9nXDWlp3v9dgjl8ok+VRtkk/VVZNyAcfkY5OCt7wXv9IudtcWuNe60TGlXTBvFKe8xxR3M78IR/5DrImxJCeJZfc42/+66W+55fiOiiU5OTxWTbrhuBmJiYlcvHiRjh07WrYZjUZ+/fVXVqxYwd9//42zs/WSPQEBAZY3noukpqYCJd+AruoC3bRkFppIyzPhdKV41WnMQ5tHb02jaELnjAKVhb9n81pbL+DqMkUtapX/TQIhhBAVZ5NneItf/Pz8/PDz8+OXX35h+fLl+Pn5UatWLYBSL3ZFF7qAgABSU1OtJq9QVZWLFy9aHXOjC2ZgYOB14wQGBt6wLUIIIYQoXe/evfn111/Ztm2b5aNNmzY8/PDDbNu2rdQ3viMjI9mxYwd5eXmWbfHx8dSpU4eQkBBHNt8mQr10OGkVTMXuWRp56fhnE+uRAvF/57PtnHnWZo2ikFmgkp4vE1gJIYQj2aTgvdHFLzQ0lMDAQOLj4y3fk5eXx44dOyzP7EZGRpKdnU1iYqLlmMTERHJycqyOudEFs3379lZxio4pOkdISMgN2yKEEEKI0vn4+NCsWTOrDzc3N3x9fWnWrBmKojB9+nT69etn+Z5Bgwbh6urKU089xeHDh4mLi2PBggU89dRT1x1dVVUpikLzWnoKTdbbh4a6EeZlPXhu/sEsMgrMB7rpFI5nGqvFzNRCCFFT2KTgLc/Fb9y4cSxYsIC4uDgOHz7MU089hbu7O4MGDQIgPDyc+++/n4kTJ7Jr1y4SExOZOHEiPXr0sAwbK88Fc+zYsWzdupX58+fz559/Mn/+fLZt28a4ceMAytUWIYQQQty68+fPc+LECctrb29v1qxZw7lz54iOjubFF19k/PjxTJgwoRJbWTF6jUJTXx25xaZu1mkUJrf2pPhjumn5Ku/9nm15XWhUOXdZenmFEMJRbDpL8/U8++yz5Obm8uKLL5Kenk7btm2JjY3F09PTcsyyZcuYPHkyDz30EAA9e/a0mu256II5adIkoqOj8fHxKXHBjIqKYsWKFcycOZNZs2bRsGFDVqxYQbt27W6qLUIIIYQon++++87qddFSgMU1b96cDRs2OKpJDuHlpCXEQ+V0tgEXrbkPIdRbxz/C3Pjkz8uW4378K5/ouvl0CnLGRadwJttIoKsWrab69W4LIUR1Y7eC99qLn6IoTJ06lalTp5b5Pb6+vpblg8pSngtm//796d+/f5n7y9MWIYRj+Xx0vUmD3MqcVCh95B32aZAQQpRDXXcdGQUqOYUmdFcK2H+EubH9fD7HM6/25M47kE3LWno8nTRoFTiRZSDUW1/WaYUQQtiITYY0CyGEEELcrsJ9dIBieTZXr1GY3MqT4h24F/NNvH/YPLRZp1FIyTWRb5RneYUQwt6k4BVCCCGEqACNotCils6qgG3io2doY+tZm384k09CsnnW5qJlioQQQtiXFLxCCCGEEBXkotMQ5qPnsuFq0fvPJm408NBaHff2gWyyC03mZYoKZZkiIYSwNyl4hRBCCCFswM9FS6CrxtLT66RVmNLG0+pm60KeiQ8O5wDgplU4JssUCSGEXUnBK4QQQghhI428dLhoFYxXitgIHz1DGrtaHbP+dB67LxQAYJBlioQQwq6k4BVCCCGEsBFFUWheS4/BdHXbiHB36rtbD23+9/4sLhtMlmWKjCbp5RVCCHuQglcIIYQQwoZ0GoVmvjpyrwxtdtYqTG7tSfFVd5NzTSy5MrS5aJkiIYQQticFrxBCCCGEjXk6aanvriXvStHbopaeQY2shzavPZXH3tQC8zJFeSZyC02lnUoIIUQFSMErhBBCCGEH9Tx0eDtpMFwZrvxEuDt3lDK0Odeg4qqBY9LLK4QQNicFrxBCCCGEnYT76FBRUFUVF53C5FYeVkOb/75sYvkfOSiKQlahSlqeTGAlhBC2JAWvEEIIIYSdaBSFFr46y9DmO/2cGNjQemhz7IlcDlwsxE2rcDxLlikSQghb0lV2A4QQVZvPR39dZ68bbC+5P33kHfZrkBBCVDOueg1hPnqSMgy4ahWejHBnR3I+5y6bn9lVgbn7s1h+jy+KAn/lGKnnIbdoQghhC9LDK4QQQghhZ7VdtAS4aMg3qrjqFF5q5Wm1/2yOkRVHc3DWKvyVI8sUCSGErUjBK4QQQgjhAI28dDhrFUyqSpvaTvQLcbHav/p/uRy6VIhWgeOZMoGVEELYghS8QgghhBAOoCgKzWvpKVp9aGwzdwJdr96KmYA5+7MwqpCaL8sUCSGELUjBK4QQQgjhIHqNQlNfHZcNKm46DS9eM7T5dLaRj//MkWWKhBDCRqTgFUIIIYRwIC8nLcEeWvKMKu38negdbD20+ZtjuRzNMJAtyxQJIUSFScErhBBCCOFg9Tx0eDtpMJhUxjVzx9/Femjz7H1ZaIFjmQZZpkgIISpACl4hhBBCiEoQ7qNDBdx1CpPu9LDadzLLyGdJlzGp5mWKhBBC3BopeIUQQgghKoFGUWjhqyfPqBIV6EyPes5W+784dpnT2Qb+yjFikE5eIYS4JVLwCiGEEEJUEle9hlBvPblGlQnNPfBzLja0WTUPbTapKmdzlUpspRBCVF+6ym6AEEIIIcTtzN9VS0aBiUt5Jl6404OXd2Va9h3PNPLN8Vw6OcMfaYVoFNAqoNOYZ3zWaRScNKDTKFf2KZZjNIoUyUIIIQWvEEIIIUQla+ylI7uwkA6BTtx/hzM//ZVv2fdZ0mVCmyjkGa+OazaqKqoKRtXcE6yiogIKRUWuigYFRQHNlY+iYlijgBZzQawooNeATlHQKeCkBV8XrRTLQogaQwpeIYQQQpTbsmXL+Oijjzhz5gwAERERTJo0iR49epT5PZs2bWL27NkcOXIEJycnoqKieOONNwgNDXVUs6s8RVFoXkvPngsFPN3Cg99SC0jLNxe4RhWWn3aiQ6iKTmMuRLWKAkrxGzkFg0klx6CSU6iSXWgix6CSXaiSbbjyutD8Osdwzf5Ck2W7q06hax1n5kZ5cYeHvlJ+FkIIYUtS8AohhBCi3OrWrcv06dNp3LgxJpOJr776imHDhvHzzz/TokWLEsefPHmSoUOHMmbMGJYsWUJ2djavvfYagwcPZu/evZWQQdWl1yhE+Og4nGZgYktPXt19dWjzyVwN03/LxNdZYylUc64UqkUFrS2W7M0uVPnudB47Uwp4s50Xj4a5V/ykQghRiWwyadX8+fOJjo6mfv36NG7cmCFDhnD48GGrY1RVZdasWURERBAUFETv3r05cuSI1THp6emMHj2a4OBggoODGT16NOnp6VbHHDp0iF69ehEUFETTpk2ZM2dOifXp1q5dS1RUFAEBAURFRbFu3bqbbosQQgghSurduzcPPPAAjRo1IjQ0lGnTpuHh4cGuXbtKPX7//v0UFhby2muv0ahRI+68804mTpzIiRMnuHjxooNbX/V5O2up564lMsCJ6LrWszZvO19A3Kk8Nv2VT0JKAb+nGTiZbSQ1zzbFbnEX80yM3Z7OPzdfJC1flkUSQlRfNil4t2/fzhNPPMHGjRuJi4tDp9MxYMAA0tLSLMcsXLiQ999/nzlz5rB582b8/f0ZOHAgWVlZlmNGjRrFgQMHWLVqFatXr+bAgQOMGTPGsj8zM5OBAwcSEBDA5s2bmT17NosWLeK9996zHJOYmMjjjz/O4MGD2bZtG4MHD2bEiBHs3r37ptoihBBCiOszGo18++235OTkEBkZWeoxrVu3Rq/X8+mnn2I0GsnKyuKrr77irrvuws/Pz8Etrh7qe+rwctIwvpk73k6V+yxt3Kk8Oq5J4btTuZXaDiGEuFU2GdIcGxtr9XrJkiUEBwezc+dOevbsiaqqxMTE8Nxzz9G/f38AYmJiCAsLY/Xq1YwcOZKjR4/y008/8cMPPxAVFQXAO++8Q8+ePUlKSiIsLIxVq1aRm5tLTEwMrq6uNGvWjD///JPFixczYcIEFEUhJiaGLl26MGnSJADCw8PZtm0bMTExfPjhh+VqixBCCCHKdujQIbp3705eXh7u7u58/vnnNG/evNRjQ0JCWLNmDSNGjGDSpEmYTCbuvPNOVq9efcM4SUlJFW6rLc5RGTQqZGZp+GcdhfdOOaFy48JXQcVVC24aFTctuGrNn920V1+7W7Zf3eeqAXetik6BdSk6vk/RWcU7n2viH5sv0ivAwEuNCnGz0QNx1fV3UxbJp2qrSfnUpFyg4vmEhYVdd79dnuHNzs7GZDLh4+MDwKlTp0hOTua+++6zHOPq6kqnTp1ISEhg5MiRJCYm4uHhYSl2ATp06IC7uzsJCQmEhYWRmJhIx44dcXV1tRzTrVs33nzzTU6dOkWDBg3YtWsXo0ePtmpPt27dWLp0abnbIoQQQoiyhYWFsW3bNjIyMoiLi2PcuHGsX7+eZs2alTg2OTmZp59+mkcffZSHH36Y7Oxs3nrrLUaMGMG6devQaMoebHajm5gbKXrDvLoKKTQRdLGA0DsMxB9Pxc/HG3e9Bg+9Yv7QKXjoNbjrzK9ddUq5Zlc2mFQKTOavNZhnZnbWKjhrFbo2U+j5dz6z92ZxNufqUGYVhe9S9By87MLizj50ruNSodyq++/mWpJP1VaT8qlJuYBj8rFLwTtlyhRatmxpGd6UnJwMgL+/v9Vx/v7+nDt3DoCUlBT8/PxQiv2hVhSF2rVrk5KSYjmmbt26Jc5RtK9BgwYkJyeXGqfoHOVpixBCCCHK5uTkRKNGjQBo06YNe/bsYfHixVaPGBVZtmwZbm5uzJgxw7Jt6dKlNG/enISEBDp27Oiwdlc3rnoNYd56FEXBt46B4Prln0BKVc1FrUE199XqNeaC1lmj4OaswdNJwU2nwUlbskAeFqolsrYTM/dmEncqz2rf6Wwj/TZe5MkIN2a088ZZZ5On44QQwm5sXvC+/PLL7Ny5kx9++AGtVmu1T7nmXUdVVUsUuNe60TFFE1bd6Jhrt5XnmOJutqvdkUMNamIsyakqxXJzUPybj+PIWLf+M626P7+amJMjY5UVpya9815eJpOJgoKCUvfl5uaWuB8oem0ymezeturO31VLeoGJc2X8qMy9tSoqRevoKjhrwFmrwctJg7tewUVbvp7fIoqi0MRXz7KutYhOymHWvixScq82wKTCkiOX2fxXAcu6+tK6tlNF0xRCCLuxacE7depUYmNjWbduHQ0aNLBsDwwMBMy9sPXq1bNsT01NtfS0BgQEkJqaalV4qqrKxYsXrY4p6qktfg642mMbGBhY6jHF99+oLaW5mRsYRw41qImxJKcqFmv7Xzf9LbcU/xbiODLWLf9Mq/DPrybm5MhYt2NhC/D666/TvXt37rjjDrKzs1m9ejXbt29n5cqVAEyfPp3ffvuNuLg4ALp3787ixYuZPXs2gwcPJisrizfeeIN69erRunXrykyl2gj10nFYgcsGFUVR0WsUnDTm3lp3Fw1eTgquOg16jW0nuHLWKoyM8KBzkDP/2pXBxrP5VvuTMg08sP4Cz7b0YGobL7Q2ji+EqB5UVSXXaF4DvPiyaZavDSqXDVfWAr+yVnhOoYkCE0wMsn/7bFbwTp48mdjYWNavX0+TJk2s9oWEhBAYGEh8fDx33XUXAHl5eezYscMyxCkyMpLs7GwSExMtz/EmJiaSk5NjeR0ZGcnrr79OXl4eLi7mZ0fi4+OpU6cOISEhALRv3574+HieeeYZS/z4+HjLOcrTFiGqOp+Prndz7lbmzXv6yDvs0yAhxG0jOTmZ0aNHk5KSgpeXF82bN2f16tV069YNgPPnz3PixAnL8V27dmX58uUsXLiQRYsW4eLiQrt27Vi9ejXu7rLGa3koikITD5UGtZ1w1nJTvbW2EOaj5/Nufnz0Rzaz9mWRln91OchCFd4+kM0PZ/NY0bUWTXz0Dm2bEMI2VFXlQp6JYxkGjmUaSMk1mdf6LipQDSYuFy9YrxSwRUWteuMQpXo20KZplMomBe+kSZP45ptv+Pzzz/Hx8bE8J+vu7o6HhweKojBu3DjmzZtHWFgYoaGhvP3227i7uzNo0CDAPJvy/fffz8SJE1m4cCGqqjJx4kR69OhheRd90KBBzJkzh6eeeopJkyZx7NgxFixYwEsvvWTpFR47diy9evVi/vz59OnTh/Xr17Nt2zZ++OEHgHK1RQghhBCli4mJuen9Dz/8MA8//LC9mnRb0Cngqqu8HlS9RmF0M0/urevCizvT2XLOegj775cMdIlL4ZU2nkxo4enwolwIUT5ZhSaOZxg4nmkubIsK3OMZBjILb7VsvXWHMhXC7RzDJgXv8uXLASzL/BSZPHkyU6dOBeDZZ58lNzeXF198kfT0dNq2bUtsbCyenp6W45ctW8bkyZN56KGHAOjZsydz58617Pf29mbNmjVMmjSJ6OhofHx8GD9+PBMmTLAcExUVxYoVK5g5cyazZs2iYcOGrFixgnbt2lmOKU9bhBBCCCGEtSY+er7tXpuYQ9nM3Z9FVrEb5HwjvLo7i+9O57HsHl+CX/4ULwAAIABJREFUPaW3V4jKUGBUOZl1tZA9lnn16/O5VWvuBL3W/kW2TQre9PT0Gx6jKApTp061FMCl8fX1tSwfVJbmzZuzYcOG6x7Tv3//EsX3zbZFCCGEEEKUpNMoPN3Skx71nHn613QSUgqt9iekFNLxPxd4s70Xw8PdrzspqBDi1phUlb9yjOae2gzr4vZUthGTgztrXbTgrjNPlOemMy+b5q43vzZ/rVj2F98XUnjZ7m2zy7JEQgghhBCiZmvi68T3PWuz4GA2b+/PJtd49Q47x6Dy3I4M1p7K44MuvgS6aa9zJiHE9RQYVfakFrD9fAE7Tjlx7lAy/8s0kGe88ffeDFetQqi3llAvPcEeWjydzOt8X1ukXlvAuusUdLc4aZ0jFjGRglcIIYQQQtwSrUbDC6286FXfhXHb09l30bq3N/7vfCLXJDO/ow8PN7q1pcaEuN0UL3C3n88nIbmg2BtKOsBwy+fWKRDiqSXUW0+ol45QLx2Nvc2f67hpauSIDCl4hRBCCCFEhTSt5cSmPrWZsy+LhQezyS/2mGBGgcoTW9JYezKXhXf74uusqbyGClEFXb/AvTV13TSEeusI9dJbCtpQLx3BnlqbL2FW1UnBK4QQQgghKkyr0fDyXd70DXFlzLY0DqdZ90LFncrj1+Rk3u/sQ6NKaqMQVUGBUeW31AK2n8tn+/kCElNurcD1dlII9dIR5q2z9Ng29tbRyFOLu17eWCoiBa8QQgghhLCZln5ObOnrz5t7s3jv92wMxe7jU/NMDPnpEn0CnHildiFNvHVob7PeJnH7sUWBW8dNQ5cgZxpr0rm3yR2Eeuuo5VwzhyDbmhS8QgghhBDCpvRaDa+386ZfA1dGb7nEsUzr2XXWp+hY/58UXLTQwldPW38n2vo70aa2nsZeOlnHV1Rr+UVDlG1Q4Hau40znIGcaempRFIWkpIuEBTrbqeU1kxS8QgghhBDCLu6q7cSvAwJ5dVcGS4/kcO0KoHlG2J1ayO7UQjiSA4C7TuHOWnra+utpU9uJNrWdLDf7QlRFlw0m9l8stEuBKypOCl4hhBBCCGE3TlqF2R18GNDQldFb0zidff21VHIMKjtSCtiRUgCYi2AvvULr2nruulIAt/YzL5siBYFwFFVVOXfZRFJGIUkZBv7MMK9/+2eGgbM5N78+UB03DZ2DnOkiBa7dScErhBBCCCHsrkOgMwkDA1n0exZxx9I5nacjs7B8vWCZhSpbzxWw9VyBZZuvs0IbP/Mw6Na1nWjjp+cOd8cWDQaTSq5RpfDarmtRbeUaVI5nGiyFbdHHsQwD2YZbnzlZCtzKIwWvEEIIIYRwCFedwkutvRjglswdDe/gcJqB3y4UcPBSIUfSzb1l2eUsgtPyVTb/nc/mv/Mt2/xdNFYFcANPHflGc1GaZ7jms1El16CSZ4Q8g8plo4k8AyWOKfo6t9j3F31vUf2j4Mod+8/TyEtHQ08tDT11NPTS0eDK115OMmNuVaKqKsm5pmK9tIWW3toz2UYqtiCQWZCrhi51pMCtCqTgFUIIIYQQDqUo4K7X0j5AS/sA8wQ8qmouJg+lF5KQbC6C/7hSBF8uZ8/ahTwT/z2bz3/P5t/4YBtSUTibY+RsjpGt50ru93PW0NDraiHc0PNqYRzgKjPt2ku+0dxbuzVVy9rLWZbCNinDQFY531gpDwUI9tASGeAkBW4VJAWvEEIIIYSodIqi4KpXaOfvTDv/q7PQ5hlMHEorZMeVIvhwWiHHMoy3tG5pZbmYb+LiBRO7LxSW2OeuUwgp6hX21FkK40ZeOuq5a9HJsk03lGtQScoo5Gi6gaPpBv5IN79ZciLLgPmfiTOQWeE4HjqFMG8dYT46mnjrzV9762jkqcNFJ7+nqkoKXiFsyOejv8rY4wbbS9+XPvIO+zVICCGEqOZcdBra+jvTtlgRnG80cfBiITtTCth/8UoRnGkg/+bnDqoQDeCkNc82fatyDCqH0wwcTjOU2KdToL7H1Z7hUC8dzXx1NPXV4+9y+/UM5xSaSMow8Ee6gaPp5mHwR9MLOZllm2HIYO6treeuJcxbRxMf3ZWi1lzcBklvfLUkBa8QQgghhKhWnLUa2gU40y6gWBFsMHHgUgEJyYXsu1jA4XQDmQUmnDUKTloFF62Cs9ZcQDtrwEWn4Hplu4tWwU1n/vDQmz+76zXmzzoF1yuf3fQaq+P1GnPP9ME/ktAHNeBEpoH/ZRo4duXzySwjf+UYudW5jgwqnMgyciLLCH9bD9P2c9ZYit9mvnqa+pi/rgnPC2cVmiw9tUevFLV/pBtuOMP3zXDTKYR6mYvaJt5XC9vGXjpcpbe2RpGCVwghhBBCVHvOOg3tA1xoH+Bi2aaqqkN65Fy0EOajJ8JHX2Kf0aRyNsfIySwDJ7KMHM80cDzTwIlMA6eyjeV+PvlaF/NNbDtfwLbzBVbb67lrzYWwj/5KMWweflsVh9ym55s4ml7I0Yzixe2tLfNTljpuGurpC2lT18vca+utI9RbT1036a29XUjBK4QQQgghaqSqUNBoNQohnjpCPHV0vWafqqqk5pk4caUYPnGlGP5fpoGT2UYu5t38ekdFk2cVn7hLo0BjLx1NfXTm3uArhXBDT53NnhHOM6ikFZhIyzdxKd/8OS3fRPo1r9PyTaQVqFzINZKca7v1nOq5a4m4kl+4j44IH/MwZC8nDUlJSYSF+dgslqhepOAVQgghhBCiEiiKgr+rFn9XLZEBJfdnF5o4mWXkRJa5CD6SVsjhNANHMwpv6nllk4plPdm4U3mW7c5aCPfW09T3SiHsY/46JV+h4FKhpYC99sO6oFW5lG9yyCRiRbMhF7Uz3EdPxJXnbN311X8ot7APKXiFEEIIIYSogjz0GlrU0tCilvVQaaNJ5WSWkcPphRxJK+TQpUIOpRVyIsvIzdSd+UY4cKmQA5cKgdxie1yBFFukcEu0CoR4aonw0dPMx9xjG+5jfsZWnq8VN0sKXiGEEEIIIaoRrUahsbeOxt46+oa4WrbnG1WSMgwcTrtSCF/pEbblM7G2pFOgoaeOCF/zcOsIHz0RvuaJo5y1UtgK25CCVwghhBBCiBrAWavQopa+RI9wZoF51uMj6eYlnA5dMi/pk3oLzwiXRaeAt5MGH2eFWs4afK98+F35XMtFg6/T1e0+zhrucNeil3WGhZ1JwStqvLLXxoWy1seVtXGFEEIIUVN4OWloH+BE+wAnq+0Xco0cSbfuET6WYQCTCT83nbk4ddLg56KhlrOGWi5afIsVtD5OV4tZD51SJSYJE+JaUvAKIYQQQghxGyqaMOueOs5W282zGodVUquEsC2ZzkwIIYQQ5bZs2TI6depE/fr1qV+/Pg888AAbN2687veoqsrixYtp3749AQEBhIeH8/rrrzumwUIIIW5r0sMrhBBCiHKrW7cu06dPp3HjxphMJr766iuGDRvGzz//TIsWLUr9nldeeYWNGzcyY8YMmjdvTkZGBsnJyQ5uuRBCiNuRFLxCCCGEKLfevXtbvZ42bRoffvghu3btKrXgTUpKYunSpfzyyy+Eh4c7qplCCCEEIEOahRBCCHGLjEYj3377LTk5OURGRpZ6zPfff0+DBg346aefaNWqFS1btmTs2LFcuHDBwa0VQghxO7qte3iXL1/Ou+++S3JyMhEREcyaNYtOnTpVdrNuG2XPnlz6zMkgsycLIURVcOjQIbp3705eXh7u7u58/vnnNG/evNRjT548yZkzZ4iNjWXx4sUoisK0adN49NFH+fHHH9Foyn7vPSkpqcJttcU5qoqalAtIPlWd5FN11aRcoOL53GiCtdu24I2NjWXKlCnMmzePDh06sHz5cgYPHszOnTupX79+ZTdPCCGEqLLCwsLYtm0bGRkZxMXFMW7cONavX0+zZs1KHGsymcjPz2fJkiWEhoYCsGTJEtq1a8eePXto167ddeNURE2aabYm5QKST1Un+VRdNSkXcEw+t+2Q5vfff5+hQ4cyfPhwwsPD+fe//01gYCArVqyo7KYJIYQQVZqTkxONGjWiTZs2vPbaa7Rs2ZLFixeXemxgYCA6nc5S7AI0btwYnU7H2bNnHdVkIYQQtyklPT1drexGOFpBQQF16tThww8/ZMCAAZbtkyZN4vDhw3z//feV2DohhBCieunbty916tRh6dKlJfZt3ryZhx56iL1799KwYUMATpw4QZs2bdi0aRNt27Z1dHOFEELcRm7LHt6LFy9iNBrx9/e32u7v709KSkoltUoIIYSo+l5//XV+/fVXTp06xaFDh5g+fTrbt29n8ODBAEyfPp1+/fpZjr/33ntp1aoV48ePZ//+/ezfv5/x48fTrl072rRpU1lpCCGEuE3cts/wAiiKYvVaVdUS24QQQghxVXJyMqNHjyYlJQUvLy+aN2/O6tWr6datGwDnz5/nxIkTluM1Gg3ffPMNkydPpnfv3ri4uBAdHc2bb7553QmrhBBCCFu4LQtePz8/tFptid7c1NTUEr2+QgghhLgqJibmpvcHBQXxySef2KtJQgghRJluy7dWnZycaN26NfHx8Vbb4+PjiYqKqqRWCSGEEEIIIYSwpduyhxdg/PjxjBkzhrZt2xIVFcWKFSs4f/48I0eOrOymCSGEEEIIIYSwgduyhxfgoYceYtasWfz73/+mS5cu7Ny5k5UrVxIcHFzZTRNCCCFue/Pnzyc6Opr69evTuHFjhgwZwuHDh62OUVWVWbNmERERQVBQEL179+bIkSNWx6SnpzN69GiCg4MJDg5m9OjRpKenlxrz+PHj1KtXjzvuuKNa56OqKosXL6Z9+/YEBAQQHh7O66+/Xm3z2bRpEw888AD16tWjUaNGPPbYYxw7dqzK5fL222/To0cP6tati4+PT6mxzpw5w5AhQ6hbty6NGjXipZdeoqCgwGa5ODKfgwcP8sQTT9C8eXOCgoJo164d7777LiaTqVrmU9zFixdp2rQpPj4+XLx4sVrn880339C5c2cCAwNp1KgRY8aMqZa57Nmzh/79+xMSEkJwcDD9+vXjt99+K1c7b9uCF2DUqFEcPHiQlJQUtmzZwt13322T83766adkZGTY5FxVxZ49e1i7di179+612Tlt/QekPNLS0qxeJyQksGPHDnJzc20e68yZM+zevZu9e/faNdfs7Gy2b99ObGwsa9asYfv27WRnZ9stXmkMBgNnzpxxaEx7+fvvvzl58qTd48ycOZPU1FS7xwHz/ASFhYV2O39eXh7/+c9/WLRoEWvXriUvL89m5963b5/NzlUeOTk57Nu3j/z8fAByc3NZtWoV33zzDefOnXNoW25327dv54knnmDjxo3ExcWh0+kYMGCA1d/xhQsX8v777zNnzhw2b96Mv78/AwcOJCsry3LMqFGjOHDgAKtWrWL16tUcOHCg1Bu+goICHn/8cTp16lTt83nllVf48MMPef3110lMTGTlypU2z8tR+Zw8eZKhQ4fSsWNHtm7dyn/+8x/y8vIss4JXpVzy8/Pp06cP48aNKzWO0WhkyJAhZGdn8/333/Phhx8SFxfHK6+8YrNcHJnPvn378PPz44MPPmDnzp1MnTqVuXPn8s4771TLfIp76qmnaNmypU3zqIx8PvjgA1599VWefvppduzYwbp16+jVq1e1yyU7O5uHH36YoKAg/vvf//Ljjz8SFBTEQw89ZHWestyW6/Dam7+/P9u3byc8PNxm59y8eTP33HMPOp15FPqqVatYuHAh//vf/wgMDGTMmDGMHTvWJrFmzJhBp06duP/++0lNTeWxxx5j9+7d6HQ6DAYDkZGRfPHFF9SuXbtCcWrVqkWXLl0YPnw4ffv2Ra/X26T9pTl+/DiPPPIIJ06coG3btnz99dcMHz6cX375BYB69erx7bff0qRJkwrHWr58OQsWLODvv/+22h4ZGcns2bNp3bp1hWOAuch85ZVX+PTTT8nLy0Or1QLmC6qLiwvDhw/njTfesOvPtcjBgwfp2rUrly5dqtB5VFVlwYIFxMXF4ePjwxNPPEGfPn0s+1NSUoiIiKhwHIDMzEyee+45duzYQefOnXnvvfeYMmUKH3/8MYqiEBkZyTfffIO3t3eF4lz7JguY8wwPD+e7774jLCwMAF9f3wrFAfj444957LHHcHZ2RlVV5s+fz7vvvktWVhYuLi6MGDGCmTNnVnhm3HHjxtGrVy/69u3LyZMn6du3L6mpqQQFBZGcnIy/vz9r166lQYMGFc7J19eXBg0aMHz4cIYOHUpAQECFz1mWPXv28NBDD5GRkUFwcDBr1qzhscce4+zZsyiKglar5dtvv6Vdu3Z2a4MoW3Z2NsHBwXzxxRf07NkTVVWJiIjgySefZNKkSYD5DYqwsDDeeOMNRo4cydGjR4mKiuKHH36gQ4cOAOzYsYOePXuya9cuy/9/AFOnTiUjI4O7776bl156ib/++qta5pOUlETHjh355ZdfbHofUln5rF27lpEjR3LhwgXLdW7r1q3069eP48eP4+fnVyVyKW7t2rUMHz68RE/1jz/+yCOPPMLBgwepV68eYO59e+aZZ0hKSsLLy8vmudgzn9K8+uqrbNmyhS1bttglF7B/PjExMWzYsIEXXniB/v372+3fmb3zSU9Pp1mzZnzxxRdER0fbrf2OyGXv3r1ER0ezb98+y73FyZMnLXMy3WiJu9u6h7ei6tevX+qHwWDgvvvus7y2hUGDBllunNeuXcvYsWPp0KED8+bN48EHH+S1115j9erVNon11VdfERgYCMC0adNQVZXdu3dz4cIF9uzZg06ns8m7kaqqYjQaefLJJ4mIiOBf//oXR48erfB5SzNt2jRCQkJYu3YtYWFhDBo0CEVROHToEEeOHKFJkya89tprFY6zaNEi5s2bxzPPPMOCBQsICwtjypQprFy5kpCQEHr16mWzXvJXXnmFuLg4Fi5cyLFjx0hNTSU1NZVjx47x7rvvEhcXx7Rp02wSy1Hee+895s+fzz333EPDhg0ZNWoUb7zxhtUxqmqb9+jeeOMNfv/9d5577jnOnTvHiBEjSEhIYMOGDaxbt4709HQWLlxY4TiNGzcu8REaGorBYODBBx+kUaNGNG7c2AYZwfPPP09mZiZgLn7nz5/PCy+8wLp165g2bRqff/45y5cvr3Cc//73v4SGhgLwr3/9i2bNmnH06FH27t3Ln3/+SevWrZk6dWqF4xSJjIzknXfeoUWLFvzf//0fmzZtstm5i5s+fTrdu3dn3759PPzwwwwaNIimTZty8uRJTp48SY8ePZgxY4ZdYosby87OxmQyWYa7nTp1iuTkZO677z7LMa6urnTq1ImEhAQAEhMT8fDwsJqQskOHDri7u1uOAdi4cSMbN25kzpw5DsrGfvl8//33NGjQgJ9++olWrVrRsmVLxo4dy4ULF6plPq1bt0av1/Ppp59iNBrJysriq6++4q677rJbEXIruZRHYmIi4eHhlmIXoFu3buTn59t1NIu98ilNVlbWDYcLV5Q989m/fz8LFy7kgw8+cNiyafbKJz4+HqPRSEpKClFRUTRt2pRhw4bZdRSbvXIJDQ2ldu3afP755+Tn55Ofn8+nn35KvXr1iIiIuOH337aTVtmCqqrcfffd9O/f32rbM888w/PPP0+dOnVsGqtITEwML7zwAi+//DIAjz32GHXr1mXx4sUMGjSowrEuXbpkeZfxl19+YenSpZYb8oYNG/LWW2/ZbCjRRx99RH5+Pp999hlffPEFixcvJioqiuHDhzNgwABcXFxsEichIYG1a9fSokULWrduTUhICN999x1169YFzAWxLXJatmwZ7777Lg888AAAnTp1onv37vz555/cf//9+Pj4MGPGDNasWVPhWKtXr2bFihV07drVarufnx+DBw/G39+fJ554gtmzZ1c4VqtWra6731bDZT/77DPeffddBg4cCMA///lPHn30UfLz85k5cyZQcv3sW7VhwwYWL17MPffcQ79+/WjWrBlffvmlpZdh+vTp/Otf/+LVV1+tUJzAwEDuvPNOxo8fb7l4qqrKgAEDePfddwkJCalwLkWK/5347LPPePnllxk/fjwAd999N+7u7ixZsoTRo0dXKE5OTg6urq6AuVf0iy++sPzN8PDwYOrUqfTs2bNCMYqbOXMmCxYsIDY2lk8//ZRBgwZRr149/u///o9hw4bZ7HnLffv28dNPPxESEsLkyZNZuHAhy5Yts4ySmDhxok2HgombM2XKFFq2bElkZCRgXg8YKLGcoL+/v2X4eUpKCn5+flZ/NxRFoXbt2palCc+fP8+zzz7LZ599hqenpyNSAeyXz8mTJzlz5gyxsbEsXrwYRVGYNm0ajz76KD/++KPdbuLtlU9ISAhr1qxhxIgRTJo0CZPJxJ133mmzN/ltlUt5pKSklDhHWUtl2pK98rnWvn37+PLLL1m6dOmtN7Yc7JVPTk4Oo0aNYs6cOdStW5fjx4/brtHXYa98Tp48iclk4u2332bWrFn4+voyd+5c+vTpQ2JiIm5ubrZL4gp75eLp6cn69esZOnQo8+fPByA4OJj//Oc/lvuR65Ee3grYsmULycnJ/Prrr/Tv35+hQ4cybNgwFEWhd+/eDB06lKFDh9o87vHjx0vcdPXs2dNmEziEhIRYHiYv7cKoKIpNn9GrV68eU6dO5eDBg3z11Vf4+voyYcIEwsPDefHFF20So6CgAA8PDwDc3d3RaDSW1wBeXl42ySk1NdVqWHTjxo3JzMy0PKv5j3/8g127dlU4Dpifm6xVq1aZ+2vVqmWz31NycjLR0dGMGjWq1I8BAwbYJM6ZM2e46667LK9bt27NunXrWL16tU17DAEuXLhAo0aNAKhTpw6urq5WwxubNm1qkyGNv/zyC4qiMH/+fMLCwujcuTNdunRBURTatm1L586d6dy5c4XjFCm6cTx16lSJN0PuueceTp06VeEYYWFh7N69GzD/v3Pt0KOMjAybvTFRxNXVlWHDhrFx40Z+/fVXevXqRUxMDK1atWLIkCE2iXHtTTdgGUJZ9LWtRhiIm/Pyyy+zc+dOPvvsM6vfCZR8E0xV1VJ/l2UdM3r0aB5//HHat29vh5aXzp75mEwm8vPzWbJkCXfffTedOnViyZIl/Pbbb+zZs8cO2dg3n+TkZJ5++mkeffRRNm/ezPr16/Hw8GDEiBE2nxypormUR1nH2/pvZhF751MkKSmJIUOGMG7cOKuOIFuzZz6TJ08mKirKru2/lj3zMZlMFBYWMmfOHO6//37atm3L0qVLSU1N5YcffrBJ+4uzZy65ublMmDCB9u3b89NPP7Fx40buvPNOhg4dSk5Ozg2/XwreCmjcuDE//vgjnp6edO3a1W4XkiKHDh1i3759uLi4YDQarfaZTCab/eEfMWIE06ZN49ixY4wePZpp06Zx4sQJwPxu0csvv0y3bt0qHOfaf+iKotCjRw++/PJLfv/9dyZMmMB///vfCscBc/HyySefoKoqn332GX5+fsTGxlr2r1q1yibDShs3bszmzZstr3/++WecnJwsQ8SdnZ1tdlHr3LkzL7/8cqnvkJ07d45p06bRpUsXm8Rq2rQpzZs35+mnny7147HHHrNJHD8/P86ePWu1LSwsjLi4OGJjYy2jGmyhVq1aVpOJ9erVy+p53ZycHJycnGwS5+uvv6ZHjx5ER0ezfv36Cp/zen744Qfi4uJwcXEpcRHIzc21Se/OhAkTmDZtGlu2bOH5559nypQpbNmyhXPnzrF161YmTpxI3759KxwHSr8JbNq0KXPmzOGPP/7g/ffft9kkba1bt+add97h9OnT/Pvf/6ZBgwZWPRVLliyhadOmNoklym/q1Kl8++23xMXFWT0XXvR39dqesdTUVEtvQkBAAKmpqVZvVKiqysWLFy3HbN26lTlz5uDn54efnx9PP/00OTk5+Pn58fHHH1e7fAIDA9HpdJbHDsB8bdLpdCX+vlaHfJYtW4abmxszZsygVatW3H333SxdupRffvmlwsNvbZlLeQQEBJQ4x8WLFzEajTd1nvKydz5F/vzzT/r06cNDDz1k89nAi7N3Plu2bOHLL7+0/C0oKnybNGlS4vEqW7B3PkXnKf4sv7e3N0FBQTb/W2DvXFatWsWJEydYvHgxd911F+3bt2f58uWcPXu2XPdVMqS5gvR6PbNmzeLee+9l2LBhPPHEE3Z7l+6hhx6yXBR27txJ27ZtLfsOHDhg9UxIRTz11FOcPXuWjh070rBhQ06fPk3btm0tk1a1atWKFStWVDjO9XpKgoKCePHFF23Wwzt58mSGDRvGokWL0Ov1xMbG8vTTT7N582a0Wi379++3ybONzz//PE8++SSbN2/GxcWF7777jjFjxlj+TWzfvt1mN8zz5s3jkUceoUWLFoSHh+Pv74+iKKSkpHD06FEiIiJYuXKlTWJFRUVddwSBh4eHTWYA7dChA+vWrSsxY3qTJk1Yu3atzYoogGbNmrF3717LcO1rf//79u2zySRmRcaNG0enTp148sknbfZGTmmefvppy9fbtm2zejZu165dNplIasiQIaSlpTF06FBMJhNGo9EyDB3MI07eeuutCseB6/+dcHZ2ZsiQITbr4X311VcZNGgQX3/9NbVr12bdunVMmDCBsLAwFEUhKyuLr7/+2iaxRPlMnjyZ2NhY1q9fX+L/x5CQEAIDA4mPj7eMDMnLy2PHjh2WZ60jIyPJzs4mMTHR8v9CYmIiOTk5lte//vqr1Xm///575s2bx6ZNmyyPvVSnfDp06IDBYODEiRM0bNgQML9ZbTAYbDaviCPzyc3NLdFbVPTalj28Fc2lPCIjI3n77bf566+/LI9ixMfH4+zsbLMJLYs4Ih+AP/74g379+jFgwABmzZpls/ZfyxH5rFmzxmqJqD179jBhwgTWr19vs7k2ijgin6JHtI4dO2b595adnU1ycrKydfYuAAANEElEQVRN/xY4Ipfc3FwURbF6016j0aAoSrn+DkjBayM9evQgPj6e0aNHYzAYbH7+/fv3W70uPhwXzM9QPvvsszaL99ZbbzFy5Eg2bNhgeQYgMDCQDh06cO+999qkqJ88eTLu7u42aO2NdevWjYSEBPbt20ebNm0IDg7m+++/Z9myZeTm5vL6669zzz33VDjOwIED8fDwYOXKleTn5zNr1iyGDx9u2T9gwACbDf+tV68e27dvZ9OmTezatcvy7llkZCQzZszgvvvus9mzWjd6Drhhw4Y26bl87rnnypy4IyIigri4ONauXVvhOABLly697s+nVq1aNl8qolWrVvz888+89NJL1K1b1yY9yMWVNiN0cQEBATaZnA1g7NixPPbYY8THx5f4G2HLG4P333/fbjOXXuuuu+7i4MGDJCUlERoaioeHB+vWrWPlypXk5eURHR1tNexd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\n",
      "text/plain": [
       "<Figure size 1008x288 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = plt.subplots(ncols=2, figsize=(14, 4))\n",
    "combined.year.value_counts().sort_index().plot.bar(title='Reviews per Year', ax=axes[0]);\n",
    "sns.lineplot(x='year', y='stars', data=combined, ax=axes[1])\n",
    "axes[1].set_title('Stars per year');"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:40:38.395097Z",
     "start_time": "2018-11-26T06:40:38.347029Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0     165419\n",
       "1     320468\n",
       "2     575835\n",
       "3     783098\n",
       "4     848641\n",
       "5     848339\n",
       "6     843802\n",
       "7     869333\n",
       "8     613929\n",
       "9     424050\n",
       "10    226052\n",
       "11    119301\n",
       "12     39268\n",
       "13      8140\n",
       "14       225\n",
       "Name: member_yrs, dtype: int64"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "combined.member_yrs.value_counts().sort_index()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:40:38.453826Z",
     "start_time": "2018-11-26T06:40:38.396927Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.0    14.989141\n",
       "2.0     8.112505\n",
       "3.0    11.057300\n",
       "4.0    21.971388\n",
       "5.0    43.869666\n",
       "Name: stars, dtype: float64"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "combined.stars.value_counts(normalize=True).sort_index().mul(100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Create train-test split"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:54:02.322183Z",
     "start_time": "2018-11-21T16:54:01.675175Z"
    }
   },
   "outputs": [],
   "source": [
    "train = combined[combined.year < 2018]\n",
    "test = combined[combined.year == 2018]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T16:57:34.506035Z",
     "start_time": "2018-11-21T16:54:04.469343Z"
    }
   },
   "outputs": [],
   "source": [
    "train.to_parquet(parquet_dir / 'train.parquet')\n",
    "test.to_parquet(parquet_dir / 'test.parquet')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:43:57.832860Z",
     "start_time": "2018-11-26T06:43:35.086606Z"
    }
   },
   "outputs": [],
   "source": [
    "train = pd.read_parquet(parquet_dir / 'train.parquet')\n",
    "test = pd.read_parquet(parquet_dir / 'test.parquet')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Benchmark Accuracy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the most frequent number of stars (=5) to predict the test set achieve an accuracy close to 51%:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:45:53.948801Z",
     "start_time": "2018-11-26T06:45:53.909151Z"
    }
   },
   "outputs": [],
   "source": [
    "test['predicted'] = train.stars.mode().iloc[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:45:56.989648Z",
     "start_time": "2018-11-26T06:45:56.969783Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.5096963305260762"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "accuracy_score(test.stars, test.predicted)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Create Yelp review document-term matrix"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:09:18.133401Z",
     "start_time": "2018-11-21T16:57:34.508544Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<5508238x10000 sparse matrix of type '<class 'numpy.int64'>'\n",
       "\twith 250524808 stored elements in Compressed Sparse Row format>"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vectorizer = CountVectorizer(stop_words='english', ngram_range=(1, 2), max_features=10000)\n",
    "train_dtm = vectorizer.fit_transform(train.text)\n",
    "train_dtm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:10:45.497868Z",
     "start_time": "2018-11-21T17:09:18.134872Z"
    }
   },
   "outputs": [],
   "source": [
    "sparse.save_npz(text_features_dir / 'train_dtm', train_dtm)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T20:41:49.703532Z",
     "start_time": "2018-11-22T20:41:43.251565Z"
    }
   },
   "outputs": [],
   "source": [
    "train_dtm = sparse.load_npz(text_features_dir / 'train_dtm.npz')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:11:38.827700Z",
     "start_time": "2018-11-21T17:10:45.499840Z"
    }
   },
   "outputs": [],
   "source": [
    "test_dtm = vectorizer.transform(test.text)\n",
    "sparse.save_npz(text_features_dir / 'test_dtm', test_dtm)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:54:34.163446Z",
     "start_time": "2018-11-26T06:54:33.419690Z"
    }
   },
   "outputs": [],
   "source": [
    "test_dtm = sparse.load_npz(text_features_dir / 'test_dtm.npz')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Train Multiclass Naive Bayes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we train a Naive Bayes classifier using a document-term matrix produced by the CountVectorizer with default settings."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T20:42:27.094801Z",
     "start_time": "2018-11-22T20:42:24.352441Z"
    }
   },
   "outputs": [],
   "source": [
    "nb = MultinomialNB()\n",
    "nb.fit(train_dtm,train.stars)\n",
    "predicted_stars = nb.predict(test_dtm)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Evaluate results"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The prediction produces 64.7% accuracy on the test set, a 24.4% improvement over the benchmark:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T20:42:34.405562Z",
     "start_time": "2018-11-22T20:42:34.367343Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.6440158551434961"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "accuracy_score(test.stars, predicted_stars)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T20:43:39.611929Z",
     "start_time": "2018-11-22T20:43:39.309889Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>1</th>\n",
       "      <th>2</th>\n",
       "      <th>3</th>\n",
       "      <th>4</th>\n",
       "      <th>5</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>153289</td>\n",
       "      <td>37619</td>\n",
       "      <td>7355</td>\n",
       "      <td>3023</td>\n",
       "      <td>3896</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>25124</td>\n",
       "      <td>30016</td>\n",
       "      <td>18722</td>\n",
       "      <td>4593</td>\n",
       "      <td>2880</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>11841</td>\n",
       "      <td>17546</td>\n",
       "      <td>39995</td>\n",
       "      <td>23913</td>\n",
       "      <td>6615</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>7871</td>\n",
       "      <td>5717</td>\n",
       "      <td>22960</td>\n",
       "      <td>110209</td>\n",
       "      <td>44228</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>29517</td>\n",
       "      <td>3545</td>\n",
       "      <td>6686</td>\n",
       "      <td>135578</td>\n",
       "      <td>424924</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        1      2      3       4       5\n",
       "1  153289  37619   7355    3023    3896\n",
       "2   25124  30016  18722    4593    2880\n",
       "3   11841  17546  39995   23913    6615\n",
       "4    7871   5717  22960  110209   44228\n",
       "5   29517   3545   6686  135578  424924"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "stars = index=list(range(1,6))\n",
    "pd.DataFrame(confusion_matrix(test.stars, predicted_stars), \n",
    "             columns=stars,\n",
    "             index=stars)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "\n",
    "### Combine non-text features with the document-term matrix"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The dataset contains various numerical features. The vectorizers produce [scipy.sparse matrices](https://docs.scipy.org/doc/scipy/reference/sparse.html). To combine the vectorized text data with other features, we need to first convert these to sparse matrices as well; many sklearn objects and other libraries like lightgbm can handle these very memory-efficient data structures. Converting the sparse matrix to a dense numpy array risks memory overflow.\n",
    "\n",
    "Most variables are categorical so we use one-hot encoding since we have a fairly large dataset to accommodate the increase in features.\n",
    "\n",
    "We convert the encoded numerical features and combine them with the document-term matrix:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### One-hot-encoding "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:03:01.316248Z",
     "start_time": "2018-11-22T21:02:59.935577Z"
    }
   },
   "outputs": [],
   "source": [
    "df = pd.concat([train.drop(['text', 'stars'], axis=1).assign(source='train'),\n",
    "                test.drop(['text', 'stars'], axis=1).assign(source='test')])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:03:19.138369Z",
     "start_time": "2018-11-22T21:03:05.824537Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "Int64Index: 6685900 entries, 0 to 6685899\n",
      "Data columns (total 25 columns):\n",
      "average_stars         6685900 non-null int64\n",
      "compliment_cool       6685900 non-null int64\n",
      "compliment_cute       6685900 non-null int64\n",
      "compliment_funny      6685900 non-null int64\n",
      "compliment_hot        6685900 non-null int64\n",
      "compliment_list       6685900 non-null int64\n",
      "compliment_more       6685900 non-null int64\n",
      "compliment_note       6685900 non-null int64\n",
      "compliment_photos     6685900 non-null int64\n",
      "compliment_plain      6685900 non-null int64\n",
      "compliment_profile    6685900 non-null int64\n",
      "compliment_writer     6685900 non-null int64\n",
      "cool                  6685900 non-null int64\n",
      "cool_user             6685900 non-null int64\n",
      "fans                  6685900 non-null int64\n",
      "funny                 6685900 non-null int64\n",
      "funny_user            6685900 non-null int64\n",
      "review_count          6685900 non-null int64\n",
      "useful                6685900 non-null int64\n",
      "useful_user           6685900 non-null int64\n",
      "member_yrs            6685900 non-null int64\n",
      "month                 6685900 non-null int64\n",
      "predicted             1177662 non-null float64\n",
      "source                6685900 non-null object\n",
      "year                  6685900 non-null int64\n",
      "dtypes: float64(1), int64(23), object(1)\n",
      "memory usage: 1.3+ GB\n"
     ]
    }
   ],
   "source": [
    "uniques = df.nunique()\n",
    "binned = pd.concat([(df.loc[:, uniques[uniques > 20].index]\n",
    "                     .apply(pd.qcut, q=10, labels=False, duplicates='drop')),\n",
    "                    df.loc[:, uniques[uniques <= 20].index]], axis=1)\n",
    "binned.info(null_counts=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:05:25.101495Z",
     "start_time": "2018-11-22T21:05:21.505198Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "Int64Index: 6685900 entries, 0 to 6685899\n",
      "Columns: 118 entries, source to year_2018\n",
      "dtypes: object(1), uint8(117)\n",
      "memory usage: 848.0+ MB\n"
     ]
    }
   ],
   "source": [
    "dummies = pd.get_dummies(binned, columns=binned.columns.drop('source'), drop_first=True)\n",
    "dummies.info()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:06:17.495638Z",
     "start_time": "2018-11-22T21:06:15.660761Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "Int64Index: 5508238 entries, 0 to 6685896\n",
      "Columns: 117 entries, average_stars_1 to year_2018\n",
      "dtypes: uint8(117)\n",
      "memory usage: 656.6 MB\n"
     ]
    }
   ],
   "source": [
    "train_dummies = dummies[dummies.source=='train'].drop('source', axis=1)\n",
    "train_dummies.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Train set"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:06:24.503323Z",
     "start_time": "2018-11-22T21:06:20.675097Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(5508238, 117)"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Cast other feature columns to float and convert to a sparse matrix.\n",
    "train_numeric = sparse.csr_matrix(train_dummies.astype(np.int8))\n",
    "train_numeric.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:06:43.353223Z",
     "start_time": "2018-11-22T21:06:37.697296Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(5508238, 10117)"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Combine sparse matrices.\n",
    "train_dtm_numeric = sparse.hstack((train_dtm, train_numeric))\n",
    "train_dtm_numeric.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:08:24.531255Z",
     "start_time": "2018-11-22T21:06:43.354518Z"
    }
   },
   "outputs": [],
   "source": [
    "sparse.save_npz(text_features_dir / 'train_dtm_numeric', train_dtm_numeric)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Repeat for test set"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:08:26.072988Z",
     "start_time": "2018-11-22T21:08:24.532631Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1177662, 10117)"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "test_dummies = dummies[dummies.source=='test'].drop('source', axis=1)\n",
    "test_numeric = sparse.csr_matrix(test_dummies.astype(np.int8))\n",
    "test_dtm_numeric = sparse.hstack((test_dtm, test_numeric))\n",
    "test_dtm_numeric.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:08:35.882484Z",
     "start_time": "2018-11-22T21:08:26.074497Z"
    }
   },
   "outputs": [],
   "source": [
    "sparse.save_npz(text_features_dir / 'test_dtm_numeric', test_dtm_numeric)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T18:12:06.454426Z",
     "start_time": "2018-11-23T18:11:55.682001Z"
    }
   },
   "outputs": [],
   "source": [
    "train_dtm_numeric = sparse.load_npz(text_features_dir / 'train_dtm_numeric.npz')\n",
    "test_dtm_numeric = sparse.load_npz(text_features_dir / 'test_dtm_numeric.npz')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Logistic Regression"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We proceed to train a one-vs-all Logistic Regression that trains one model per class while treating the remaining classes as the negative class and predicts probabilities for each class using the different models.\n",
    "\n",
    "Using only the text features, we train and evaluate the model as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:08:55.069249Z",
     "start_time": "2018-11-22T21:08:55.062720Z"
    }
   },
   "outputs": [],
   "source": [
    "logreg = LogisticRegression(C=1e9)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Text features only"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T18:31:17.317673Z",
     "start_time": "2018-11-21T17:50:15.118828Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "logreg.fit(X=train_dtm, y=train.stars)\n",
    "y_pred_class = logreg.predict(test_dtm)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:51:48.392513Z",
     "start_time": "2018-11-26T06:51:48.383821Z"
    }
   },
   "outputs": [],
   "source": [
    "joblib.dump(logreg, 'train_dtm.joblib') "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:54:07.052479Z",
     "start_time": "2018-11-26T06:54:07.047514Z"
    }
   },
   "outputs": [],
   "source": [
    "logreg = joblib.load('log_reg_multi/train_dtm.joblib')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:54:42.935813Z",
     "start_time": "2018-11-26T06:54:42.717740Z"
    }
   },
   "outputs": [],
   "source": [
    "y_pred_class = logreg.predict(test_dtm)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Evaluate Results"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The model achieves significantly higher accuracy at 73.6%:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-26T06:54:49.824441Z",
     "start_time": "2018-11-26T06:54:49.801577Z"
    }
   },
   "outputs": [],
   "source": [
    "print(accuracy_score(test.stars, y_pred_class))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Combined Features"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### One-vs-all Logistic Regression"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:59:50.309518Z",
     "start_time": "2018-11-22T21:10:55.158840Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Use logistic regression with all features.\n",
    "logreg.fit(train_dtm_numeric.astype(float), train.stars)\n",
    "y_pred_class = logreg.predict(test_dtm_numeric.astype(float))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:59:50.315309Z",
     "start_time": "2018-11-22T21:59:50.311075Z"
    }
   },
   "outputs": [],
   "source": [
    "joblib.dump(logreg, 'train_dtm_numeric.joblib') "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T21:59:50.348097Z",
     "start_time": "2018-11-22T21:59:50.316228Z"
    }
   },
   "outputs": [],
   "source": [
    "accuracy_score(test.stars, y_pred_class)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Multinomial Logistic Regression"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Logistic regression also provides a multinomial training option that is faster and more accurate than the one-vs-all implementation. We use the lbfgs solver (see sklearn [documentation](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) for details)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T23:47:30.063002Z",
     "start_time": "2018-11-22T23:40:39.092482Z"
    }
   },
   "outputs": [],
   "source": [
    "multi_logreg = LogisticRegression(C=1e9, multi_class='multinomial', solver='lbfgs')\n",
    "multi_logreg.fit(train_dtm_numeric.astype(float), train.stars)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T23:47:30.068053Z",
     "start_time": "2018-11-22T23:47:30.064593Z"
    }
   },
   "outputs": [],
   "source": [
    "joblib.dump(multi_logreg, 'train_dtm_numeric_multi.joblib')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T23:47:31.312857Z",
     "start_time": "2018-11-22T23:47:30.068969Z"
    }
   },
   "outputs": [],
   "source": [
    "y_pred_class = multi_logreg.predict(test_dtm_numeric.astype(float))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case, tuning of the regularization parameter C did not lead to very significant improvements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-22T23:47:31.337396Z",
     "start_time": "2018-11-22T23:47:31.314381Z"
    }
   },
   "outputs": [],
   "source": [
    "accuracy_score(test.stars, y_pred_class)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T10:15:32.149142Z",
     "start_time": "2018-11-22T23:57:31.684556Z"
    },
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "for solver in ['newton-cg', 'sag', 'saga']:\n",
    "    start = time()\n",
    "    multi_logreg = LogisticRegression(C=1e9, multi_class='multinomial', solver=solver)\n",
    "    multi_logreg.fit(train_dtm_numeric.astype(float), train.stars)\n",
    "    joblib.dump(multi_logreg, f'train_dtm_numeric_multi_{solver}.joblib')\n",
    "    y_pred_class = multi_logreg.predict(test_dtm_numeric.astype(float))\n",
    "    print(f'{solver}: {time()-start:.2f}s | {accuracy_score(test.stars, y_pred_class):.2%}', flush=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T16:49:15.010878Z",
     "start_time": "2018-11-23T15:46:38.300352Z"
    }
   },
   "outputs": [],
   "source": [
    "for C in [1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1, 0.1,0]:\n",
    "    start = time()\n",
    "    multi_logreg = LogisticRegression(C=C, multi_class='multinomial', solver='lbfgs')\n",
    "    multi_logreg.fit(train_dtm_numeric.astype(float), train.stars)\n",
    "    joblib.dump(multi_logreg, f'train_dtm_numeric_multi_{int(C*10):d}.joblib')\n",
    "    y_pred_class = multi_logreg.predict(test_dtm_numeric.astype(float))\n",
    "    print(f'{C}: {time()-start:.2f}s | {accuracy_score(test.stars, y_pred_class):.2%}', flush=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Gradient Boosting"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For illustration, we also train a lightgbm Gradient Boosting tree ensemble with default settings and multiclass objective."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T18:29:09.279963Z",
     "start_time": "2018-11-23T18:29:01.645297Z"
    }
   },
   "outputs": [],
   "source": [
    "lgb_train = lgb.Dataset(data=train_dtm_numeric.tocsr().astype(np.float32), \n",
    "                        label=train.stars.sub(1), \n",
    "                        categorical_feature=list(range(train_dtm_numeric.shape[1])))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T18:29:09.578466Z",
     "start_time": "2018-11-23T18:29:09.281388Z"
    }
   },
   "outputs": [],
   "source": [
    "lgb_test = lgb.Dataset(data=test_dtm_numeric.tocsr().astype(np.float32), \n",
    "                       label=test.stars.sub(1), \n",
    "                       reference=lgb_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T18:29:09.582339Z",
     "start_time": "2018-11-23T18:29:09.580028Z"
    }
   },
   "outputs": [],
   "source": [
    "param = {'objective':'multiclass', 'num_class': 5}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T21:52:17.824036Z",
     "start_time": "2018-11-23T18:29:09.583871Z"
    },
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "booster = lgb.train(params=param, \n",
    "                    train_set=lgb_train, \n",
    "                    num_boost_round=500, \n",
    "                    early_stopping_rounds=20,\n",
    "                    valid_sets=[lgb_train, lgb_test])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T21:52:17.985172Z",
     "start_time": "2018-11-23T21:52:17.933310Z"
    }
   },
   "outputs": [],
   "source": [
    "booster.save_model(str(text_features_dir / 'lgb_model.txt'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T22:00:54.906351Z",
     "start_time": "2018-11-23T21:59:21.482150Z"
    }
   },
   "outputs": [],
   "source": [
    "y_pred_class = booster.predict(test_dtm_numeric.astype(float))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The basic settings did not improve over the multinomial logistic regression, but further parameter tuning remains an unused option."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T22:03:09.846172Z",
     "start_time": "2018-11-23T22:03:09.816105Z"
    }
   },
   "outputs": [],
   "source": [
    "accuracy_score(test.stars, y_pred_class.argmax(1) + 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_pred_class_class_classd_classed_classred"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T22:18:00.865341Z",
     "start_time": "2018-11-23T22:18:00.857910Z"
    }
   },
   "outputs": [],
   "source": [
    "fi = booster.feature_importance(importance_type='gain')\n",
    "pd.Series(fi).div(fi.sum()).sort_values(ascending=False).head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Naive Bayes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T22:04:57.061729Z",
     "start_time": "2018-11-23T22:04:46.049681Z"
    }
   },
   "outputs": [],
   "source": [
    "nb = MultinomialNB()\n",
    "nb.fit(train_dtm_numeric,train.stars)\n",
    "predicted_stars = nb.predict(test_dtm_numeric)\n",
    "accuracy_score(test.stars, predicted_stars)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Textblob for Sentiment Analysis"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:32:40.765746Z",
     "start_time": "2018-11-21T17:32:40.571104Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "sample_review = combined.text.sample(1).iloc[0]\n",
    "print(sample_review)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:32:40.940304Z",
     "start_time": "2018-11-21T17:32:40.934791Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Polarity ranges from -1 (most negative) to 1 (most positive).\n",
    "TextBlob(sample_review).sentiment.polarity"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-23T23:55:42.292091Z",
     "start_time": "2018-11-23T23:55:42.290276Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Define a function that accepts text and returns the polarity.\n",
    "def detect_sentiment(text):\n",
    "    return TextBlob(text).sentiment.polarity"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T01:05:21.178283Z",
     "start_time": "2018-11-23T23:56:01.144736Z"
    }
   },
   "outputs": [],
   "source": [
    "combined['sentiment'] = combined.text.apply(detect_sentiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:10:18.867787Z",
     "start_time": "2018-11-24T05:06:08.373534Z"
    }
   },
   "outputs": [],
   "source": [
    "combined.to_parquet(parquet_dir / 'combined_tb.parquet', compression='gzip')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:46:59.746135Z",
     "start_time": "2018-11-21T17:46:59.298644Z"
    }
   },
   "outputs": [],
   "source": [
    "sample_reviews = combined[['stars', 'text']].sample(100000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-21T17:48:10.849876Z",
     "start_time": "2018-11-21T17:47:01.859678Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Create a new DataFrame column for sentiment (Warning: SLOW!).\n",
    "sample_reviews['sentiment'] = sample_reviews.text.apply(detect_sentiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:03:22.544002Z",
     "start_time": "2018-11-24T05:03:21.568279Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Box plot of sentiment grouped by stars\n",
    "sns.boxenplot(x='stars', y='sentiment', data=combined);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:04:13.803888Z",
     "start_time": "2018-11-24T05:04:13.796660Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Widen the column display.\n",
    "pd.set_option('max_colwidth', 500)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:04:14.717109Z",
     "start_time": "2018-11-24T05:04:14.705564Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Reviews with most negative sentiment\n",
    "combined[combined.sentiment == -1].text.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:04:31.689717Z",
     "start_time": "2018-11-24T05:04:31.663071Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Negative sentiment in a 5-star review\n",
    "combined[(combined.stars == 5) & (combined.sentiment < -0.3)].head(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-11-24T05:05:10.547475Z",
     "start_time": "2018-11-24T05:05:10.532685Z"
    },
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Positive sentiment in a 1-star review\n",
    "combined.loc[(combined.stars == 1) & (combined.sentiment > 0.5), 'text'].head(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# Reset the column display width.\n",
    "pd.reset_option('max_colwidth')"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Slideshow",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": true,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "316px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
